@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 +39 -0
- package/dist/cli.js +49 -5
- package/dist/command-line.js +34 -11
- package/dist/options.js +32 -25
- package/dist/types/cli.d.ts +2 -1
- package/dist/types/types.d.ts +1 -0
- package/dist/types/ui-prompts.d.ts +3 -1
- package/dist/ui-prompts.js +54 -1
- package/package.json +3 -3
- package/src/cli.ts +64 -2
- package/src/command-line.ts +44 -13
- package/src/options.ts +44 -35
- package/src/types.ts +1 -0
- package/src/ui-prompts.ts +76 -0
- package/tests/command-line.test.ts +84 -0
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', '
|
|
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
|
package/dist/command-line.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
?
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -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;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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>;
|
package/dist/ui-prompts.js
CHANGED
|
@@ -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.
|
|
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.
|
|
42
|
-
"@tanstack/create-ui": "0.59.
|
|
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
|
-
'
|
|
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')
|
package/src/command-line.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
?
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
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',
|