@tanstack/cli 0.59.4 → 0.59.6
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 +26 -0
- package/dist/cli.js +39 -3
- package/dist/command-line.js +23 -9
- package/dist/options.js +13 -21
- package/dist/types/cli.d.ts +2 -1
- package/package.json +3 -3
- package/src/cli.ts +53 -2
- package/src/command-line.ts +31 -11
- package/src/options.ts +14 -25
- package/tests/command-line.test.ts +67 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @tanstack/cli
|
|
2
2
|
|
|
3
|
+
## 0.59.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Improve CLI compatibility and scaffold behavior for legacy router-first workflows. ([`2949819`](https://github.com/TanStack/cli/commit/2949819058b4d4b1760be683ef29bfd459ddb28b))
|
|
8
|
+
|
|
9
|
+
- Add safer target directory handling by warning before creating into non-empty folders.
|
|
10
|
+
- Support explicit git initialization control via `--git` and `--no-git`.
|
|
11
|
+
- Restore router-only compatibility mode with file-based routing templates (without Start-dependent add-ons/deployments/starters), while still allowing toolchains.
|
|
12
|
+
- Default `create-tsrouter-app` to router-only compatibility mode.
|
|
13
|
+
- Remove stale `count.txt` ignore entries from base templates.
|
|
14
|
+
|
|
15
|
+
Also expands starter documentation with clearer creation, maintenance, UI usage, and banner guidance.
|
|
16
|
+
|
|
17
|
+
- Updated dependencies [[`164522e`](https://github.com/TanStack/cli/commit/164522e444188e83710fc599304132de8cb379e6), [`2949819`](https://github.com/TanStack/cli/commit/2949819058b4d4b1760be683ef29bfd459ddb28b)]:
|
|
18
|
+
- @tanstack/create@0.61.4
|
|
19
|
+
- @tanstack/create-ui@0.59.6
|
|
20
|
+
|
|
21
|
+
## 0.59.5
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- Updated dependencies [[`cc5857c`](https://github.com/TanStack/cli/commit/cc5857c5c212132852f37878e039071c5a9b1ac5)]:
|
|
26
|
+
- @tanstack/create@0.61.3
|
|
27
|
+
- @tanstack/create-ui@0.59.5
|
|
28
|
+
|
|
3
29
|
## 0.59.4
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/dist/cli.js
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
|
import { SUPPORTED_PACKAGE_MANAGERS, addToApp, compileAddOn, compileStarter, createApp, createSerializedOptions, getAllAddOns, getFrameworkByName, getFrameworks, initAddOn, initStarter, } from '@tanstack/create';
|
|
@@ -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,7 +297,7 @@ 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
303
|
.option('--no-tailwind', 'Deprecated: compatibility flag; Tailwind opt-out is ignored');
|
|
@@ -295,6 +330,7 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
|
|
|
295
330
|
})
|
|
296
331
|
.option('--list-add-ons', 'list all available add-ons', false)
|
|
297
332
|
.option('--addon-details <addon-id>', 'show detailed information about a specific add-on')
|
|
333
|
+
.option('--git', 'create a git repository')
|
|
298
334
|
.option('--no-git', 'do not create a git repository')
|
|
299
335
|
.option('--target-dir <path>', 'the target directory for the application root')
|
|
300
336
|
.option('--ui', 'Launch the UI for project creation')
|
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,10 +113,10 @@ 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));
|
|
@@ -131,7 +145,7 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
|
|
|
131
145
|
packageManager: cliOptions.packageManager ||
|
|
132
146
|
getPackageManager() ||
|
|
133
147
|
DEFAULT_PACKAGE_MANAGER,
|
|
134
|
-
git:
|
|
148
|
+
git: cliOptions.git ?? true,
|
|
135
149
|
install: cliOptions.install,
|
|
136
150
|
chosenAddOns,
|
|
137
151
|
addOnOptions: {
|
package/dist/options.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
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
3
|
import { getProjectName, promptForAddOnOptions, selectAddOns, selectDeployment, selectGit, selectPackageManager, selectToolchain, } from './ui-prompts.js';
|
|
5
4
|
import { getCurrentDirectoryName, sanitizePackageName, validateProjectName, } from './utils.js';
|
|
@@ -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,7 +42,9 @@ 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();
|
|
@@ -64,10 +54,12 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], sh
|
|
|
64
54
|
if (deployment) {
|
|
65
55
|
addOns.add(deployment);
|
|
66
56
|
}
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
if (!routerOnly) {
|
|
58
|
+
for (const addOn of forcedAddOns) {
|
|
59
|
+
addOns.add(addOn);
|
|
60
|
+
}
|
|
69
61
|
}
|
|
70
|
-
if (Array.isArray(cliOptions.addOns)) {
|
|
62
|
+
if (!routerOnly && Array.isArray(cliOptions.addOns)) {
|
|
71
63
|
for (const addOn of cliOptions.addOns) {
|
|
72
64
|
if (addOn.toLowerCase() === 'start') {
|
|
73
65
|
continue;
|
|
@@ -75,7 +67,7 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], sh
|
|
|
75
67
|
addOns.add(addOn);
|
|
76
68
|
}
|
|
77
69
|
}
|
|
78
|
-
else {
|
|
70
|
+
else if (!routerOnly) {
|
|
79
71
|
for (const addOn of await selectAddOns(options.framework, options.mode, 'add-on', 'What add-ons would you like for your project?', forcedAddOns)) {
|
|
80
72
|
addOns.add(addOn);
|
|
81
73
|
}
|
|
@@ -98,7 +90,7 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], sh
|
|
|
98
90
|
// Merge user options with defaults
|
|
99
91
|
options.addOnOptions = { ...defaultOptions, ...userOptions };
|
|
100
92
|
}
|
|
101
|
-
options.git = cliOptions.git
|
|
93
|
+
options.git = cliOptions.git ?? (await selectGit());
|
|
102
94
|
if (cliOptions.install === false) {
|
|
103
95
|
options.install = false;
|
|
104
96
|
}
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/cli",
|
|
3
|
-
"version": "0.59.
|
|
3
|
+
"version": "0.59.6",
|
|
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.4",
|
|
42
|
+
"@tanstack/create-ui": "0.59.6"
|
|
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
|
|
|
@@ -55,6 +55,7 @@ export function cli({
|
|
|
55
55
|
frameworkDefinitionInitializers,
|
|
56
56
|
showDeploymentOptions = false,
|
|
57
57
|
legacyAutoCreate = false,
|
|
58
|
+
defaultRouterOnly = false,
|
|
58
59
|
}: {
|
|
59
60
|
name: string
|
|
60
61
|
appName: string
|
|
@@ -65,11 +66,43 @@ export function cli({
|
|
|
65
66
|
frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>
|
|
66
67
|
showDeploymentOptions?: boolean
|
|
67
68
|
legacyAutoCreate?: boolean
|
|
69
|
+
defaultRouterOnly?: boolean
|
|
68
70
|
}) {
|
|
69
71
|
const environment = createUIEnvironment(appName, false)
|
|
70
72
|
|
|
71
73
|
const program = new Command()
|
|
72
74
|
|
|
75
|
+
async function confirmTargetDirectorySafety(
|
|
76
|
+
targetDir: string,
|
|
77
|
+
forced?: boolean,
|
|
78
|
+
) {
|
|
79
|
+
if (forced) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!fs.existsSync(targetDir)) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!fs.statSync(targetDir).isDirectory()) {
|
|
88
|
+
throw new Error(`Target path exists and is not a directory: ${targetDir}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (fs.readdirSync(targetDir).length === 0) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const shouldContinue = await confirm({
|
|
96
|
+
message: `Target directory "${targetDir}" already exists and is not empty. Continue anyway?`,
|
|
97
|
+
initialValue: false,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if (isCancel(shouldContinue) || !shouldContinue) {
|
|
101
|
+
cancel('Operation cancelled.')
|
|
102
|
+
process.exit(0)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
73
106
|
const availableFrameworks = getFrameworks().map((f) => f.name)
|
|
74
107
|
|
|
75
108
|
const toolchains = new Set<string>()
|
|
@@ -251,6 +284,7 @@ export function cli({
|
|
|
251
284
|
console.log(chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...')
|
|
252
285
|
}
|
|
253
286
|
const silentEnvironment = createUIEnvironment(appName, true)
|
|
287
|
+
await confirmTargetDirectorySafety(normalizedOpts.targetDir, options.force)
|
|
254
288
|
await createApp(silentEnvironment, normalizedOpts)
|
|
255
289
|
console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`)
|
|
256
290
|
|
|
@@ -275,6 +309,18 @@ export function cli({
|
|
|
275
309
|
...options,
|
|
276
310
|
} as CliOptions
|
|
277
311
|
|
|
312
|
+
if (defaultRouterOnly && cliOptions.routerOnly === undefined) {
|
|
313
|
+
cliOptions.routerOnly = true
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (
|
|
317
|
+
cliOptions.routerOnly !== true &&
|
|
318
|
+
cliOptions.template &&
|
|
319
|
+
cliOptions.template.toLowerCase() !== 'file-router'
|
|
320
|
+
) {
|
|
321
|
+
cliOptions.routerOnly = true
|
|
322
|
+
}
|
|
323
|
+
|
|
278
324
|
cliOptions.framework = getFrameworkByName(
|
|
279
325
|
options.framework || defaultFramework || 'React',
|
|
280
326
|
)!.id
|
|
@@ -327,6 +373,9 @@ export function cli({
|
|
|
327
373
|
throw new Error('No options were provided')
|
|
328
374
|
}
|
|
329
375
|
|
|
376
|
+
;(finalOptions as Options & { routerOnly?: boolean }).routerOnly =
|
|
377
|
+
!!cliOptions.routerOnly
|
|
378
|
+
|
|
330
379
|
// Determine target directory:
|
|
331
380
|
// 1. Use --target-dir if provided
|
|
332
381
|
// 2. Use targetDir from normalizeOptions if set (handles "." case)
|
|
@@ -342,6 +391,7 @@ export function cli({
|
|
|
342
391
|
finalOptions.targetDir = resolve(process.cwd(), finalOptions.projectName)
|
|
343
392
|
}
|
|
344
393
|
|
|
394
|
+
await confirmTargetDirectorySafety(finalOptions.targetDir, options.force)
|
|
345
395
|
await createApp(environment, finalOptions)
|
|
346
396
|
} catch (error) {
|
|
347
397
|
log.error(
|
|
@@ -402,7 +452,7 @@ export function cli({
|
|
|
402
452
|
)
|
|
403
453
|
.option(
|
|
404
454
|
'--router-only',
|
|
405
|
-
'
|
|
455
|
+
'Use router-only compatibility mode (file-based routing without TanStack Start)',
|
|
406
456
|
)
|
|
407
457
|
.option(
|
|
408
458
|
'--template <type>',
|
|
@@ -471,6 +521,7 @@ export function cli({
|
|
|
471
521
|
'--addon-details <addon-id>',
|
|
472
522
|
'show detailed information about a specific add-on',
|
|
473
523
|
)
|
|
524
|
+
.option('--git', 'create a git repository')
|
|
474
525
|
.option('--no-git', 'do not create a git repository')
|
|
475
526
|
.option(
|
|
476
527
|
'--target-dir <path>',
|
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
|
|
128
|
+
|
|
129
|
+
const template = cliOptions.template?.toLowerCase().trim()
|
|
130
|
+
if (template && template !== 'file-router') {
|
|
131
|
+
routerOnly = true
|
|
132
|
+
}
|
|
113
133
|
|
|
114
|
-
const starter = cliOptions.starter
|
|
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
|
|
|
@@ -195,7 +215,7 @@ export async function normalizeOptions(
|
|
|
195
215
|
cliOptions.packageManager ||
|
|
196
216
|
getPackageManager() ||
|
|
197
217
|
DEFAULT_PACKAGE_MANAGER,
|
|
198
|
-
git:
|
|
218
|
+
git: cliOptions.git ?? true,
|
|
199
219
|
install: cliOptions.install,
|
|
200
220
|
chosenAddOns,
|
|
201
221
|
addOnOptions: {
|
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,
|
|
@@ -59,25 +58,11 @@ export async function promptForCreateOptions(
|
|
|
59
58
|
options.projectName = await getProjectName()
|
|
60
59
|
}
|
|
61
60
|
|
|
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
61
|
// Mode is always file-router (TanStack Start)
|
|
80
62
|
options.mode = 'file-router'
|
|
63
|
+
const template = cliOptions.template?.toLowerCase().trim()
|
|
64
|
+
const routerOnly =
|
|
65
|
+
!!cliOptions.routerOnly || (template ? template !== 'file-router' : false)
|
|
81
66
|
|
|
82
67
|
// TypeScript is always enabled with file-router
|
|
83
68
|
options.typescript = true
|
|
@@ -99,7 +84,9 @@ export async function promptForCreateOptions(
|
|
|
99
84
|
|
|
100
85
|
// Deployment selection
|
|
101
86
|
const deployment = showDeploymentOptions
|
|
102
|
-
?
|
|
87
|
+
? routerOnly
|
|
88
|
+
? undefined
|
|
89
|
+
: await selectDeployment(options.framework, cliOptions.deployment)
|
|
103
90
|
: undefined
|
|
104
91
|
|
|
105
92
|
// Add-ons selection
|
|
@@ -112,18 +99,20 @@ export async function promptForCreateOptions(
|
|
|
112
99
|
addOns.add(deployment)
|
|
113
100
|
}
|
|
114
101
|
|
|
115
|
-
|
|
116
|
-
|
|
102
|
+
if (!routerOnly) {
|
|
103
|
+
for (const addOn of forcedAddOns) {
|
|
104
|
+
addOns.add(addOn)
|
|
105
|
+
}
|
|
117
106
|
}
|
|
118
107
|
|
|
119
|
-
if (Array.isArray(cliOptions.addOns)) {
|
|
108
|
+
if (!routerOnly && Array.isArray(cliOptions.addOns)) {
|
|
120
109
|
for (const addOn of cliOptions.addOns) {
|
|
121
110
|
if (addOn.toLowerCase() === 'start') {
|
|
122
111
|
continue
|
|
123
112
|
}
|
|
124
113
|
addOns.add(addOn)
|
|
125
114
|
}
|
|
126
|
-
} else {
|
|
115
|
+
} else if (!routerOnly) {
|
|
127
116
|
for (const addOn of await selectAddOns(
|
|
128
117
|
options.framework,
|
|
129
118
|
options.mode,
|
|
@@ -168,7 +157,7 @@ export async function promptForCreateOptions(
|
|
|
168
157
|
options.addOnOptions = { ...defaultOptions, ...userOptions }
|
|
169
158
|
}
|
|
170
159
|
|
|
171
|
-
options.git = cliOptions.git
|
|
160
|
+
options.git = cliOptions.git ?? (await selectGit())
|
|
172
161
|
if (cliOptions.install === false) {
|
|
173
162
|
options.install = false
|
|
174
163
|
}
|
|
@@ -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,56 @@ 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('should ignore add-ons and deployment in router-only mode but keep toolchain', async () => {
|
|
304
|
+
__testRegisterFramework({
|
|
305
|
+
id: 'react-cra',
|
|
306
|
+
name: 'react',
|
|
307
|
+
getAddOns: () => [
|
|
308
|
+
{
|
|
309
|
+
id: 'form',
|
|
310
|
+
name: 'Form',
|
|
311
|
+
modes: ['file-router'],
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
id: 'nitro',
|
|
315
|
+
name: 'nitro',
|
|
316
|
+
modes: ['file-router'],
|
|
317
|
+
type: 'deployment',
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: 'biome',
|
|
321
|
+
name: 'Biome',
|
|
322
|
+
modes: ['file-router'],
|
|
323
|
+
type: 'toolchain',
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
const options = await normalizeOptions(
|
|
329
|
+
{
|
|
330
|
+
projectName: 'test',
|
|
331
|
+
framework: 'react-cra',
|
|
332
|
+
routerOnly: true,
|
|
333
|
+
addOns: ['form'],
|
|
334
|
+
deployment: 'nitro',
|
|
335
|
+
toolchain: 'biome',
|
|
336
|
+
},
|
|
337
|
+
['form'],
|
|
338
|
+
{ forcedDeployment: 'nitro' },
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
expect(options?.chosenAddOns.map((a) => a.id)).toEqual(['biome'])
|
|
342
|
+
})
|
|
343
|
+
|
|
277
344
|
it('should handle the funky Windows edge case with CLI parsing', async () => {
|
|
278
345
|
__testRegisterFramework({
|
|
279
346
|
id: 'react-cra',
|