@tanstack/cli 0.59.8 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/bin.js +5 -0
- package/dist/cli.js +118 -93
- package/dist/command-line.js +143 -8
- package/dist/dev-watch.js +117 -16
- package/dist/file-syncer.js +30 -1
- package/dist/index.js +15 -1
- package/dist/options.js +5 -2
- package/dist/types/cli.d.ts +1 -2
- package/dist/types/dev-watch.d.ts +6 -0
- package/dist/types/file-syncer.d.ts +8 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/types.d.ts +2 -1
- package/package.json +8 -3
- package/playwright-report/index.html +85 -0
- package/playwright.config.ts +21 -0
- package/src/bin.ts +8 -0
- package/src/cli.ts +150 -119
- package/src/command-line.ts +193 -7
- package/src/dev-watch.ts +163 -29
- package/src/file-syncer.ts +59 -1
- package/src/index.ts +21 -1
- package/src/options.ts +8 -2
- package/src/types.ts +2 -1
- package/test-results/.last-run.json +4 -0
- package/tests/command-line.test.ts +203 -15
- package/tests/options.test.ts +2 -2
- package/tests-e2e/addons-smoke.spec.ts +31 -0
- package/tests-e2e/create-smoke.spec.ts +39 -0
- package/tests-e2e/helpers.ts +526 -0
- package/tests-e2e/matrix-opportunistic.spec.ts +142 -0
- package/tests-e2e/router-only-smoke.spec.ts +68 -0
- package/tests-e2e/solid-smoke.spec.ts +25 -0
- package/tests-e2e/templates-smoke.spec.ts +52 -0
- package/vitest.config.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @tanstack/cli
|
|
2
2
|
|
|
3
|
+
## 0.60.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- This release pulls together a large batch of improvements across the CLI and scaffolding engine since the last versioning pass. ([`154b25e`](https://github.com/TanStack/cli/commit/154b25eec9a13b9718c44cbed6cb3c8566f2fb11))
|
|
8
|
+
|
|
9
|
+
- Modernizes and refreshes the generated React/Solid template experience, including updated starter content and stronger defaults.
|
|
10
|
+
- Improves create flows with better option normalization, stronger guardrails around target directories, and clearer compatibility behavior in router-only mode.
|
|
11
|
+
- Expands scaffolding ergonomics with examples toggles, improved add-on/config handling, and reliability fixes across package-manager and cross-platform paths.
|
|
12
|
+
- Strengthens test and release confidence via e2e/release workflow hardening and broader smoke coverage.
|
|
13
|
+
- Streamlines product surface area by removing the local `create-ui` package and `--ui` command paths from the CLI; visual setup now lives at `https://tanstack.com/builder`.
|
|
14
|
+
- Cleans up docs and custom CLI examples to match the current terminal-first workflow and Builder guidance.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [[`154b25e`](https://github.com/TanStack/cli/commit/154b25eec9a13b9718c44cbed6cb3c8566f2fb11)]:
|
|
19
|
+
- @tanstack/create@0.62.0
|
|
20
|
+
|
|
3
21
|
## 0.59.8
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/dist/bin.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { cli } from './cli.js';
|
|
3
|
+
import { createReactFrameworkDefinition, createSolidFrameworkDefinition, } from '@tanstack/create';
|
|
3
4
|
cli({
|
|
4
5
|
name: 'tanstack',
|
|
5
6
|
appName: 'TanStack',
|
|
7
|
+
frameworkDefinitionInitializers: [
|
|
8
|
+
createReactFrameworkDefinition,
|
|
9
|
+
createSolidFrameworkDefinition,
|
|
10
|
+
],
|
|
6
11
|
});
|
package/dist/cli.js
CHANGED
|
@@ -4,8 +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,
|
|
8
|
-
import { launchUI } from '@tanstack/create-ui';
|
|
7
|
+
import { SUPPORTED_PACKAGE_MANAGERS, addToApp, compileAddOn, compileStarter, createApp, devAddOn, getAllAddOns, getFrameworkByName, getFrameworks, initAddOn, initStarter, } from '@tanstack/create';
|
|
9
8
|
import { runMCPServer } from './mcp.js';
|
|
10
9
|
import { promptForAddOns, promptForCreateOptions } from './options.js';
|
|
11
10
|
import { normalizeOptions, validateDevWatchOptions, validateLegacyCreateFlags, } from './command-line.js';
|
|
@@ -15,7 +14,7 @@ import { DevWatchManager } from './dev-watch.js';
|
|
|
15
14
|
const packageJsonPath = new URL('../package.json', import.meta.url);
|
|
16
15
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
17
16
|
const VERSION = packageJson.version;
|
|
18
|
-
export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaultFramework,
|
|
17
|
+
export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaultFramework, frameworkDefinitionInitializers, showDeploymentOptions = false, legacyAutoCreate = false, defaultRouterOnly = false, }) {
|
|
19
18
|
const environment = createUIEnvironment(appName, false);
|
|
20
19
|
const program = new Command();
|
|
21
20
|
async function confirmTargetDirectorySafety(targetDir, forced) {
|
|
@@ -41,6 +40,73 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
|
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
42
|
const availableFrameworks = getFrameworks().map((f) => f.name);
|
|
43
|
+
function resolveBuiltInDevWatchPath(frameworkId) {
|
|
44
|
+
const candidates = [
|
|
45
|
+
resolve(process.cwd(), 'packages/create/src/frameworks', frameworkId),
|
|
46
|
+
resolve(process.cwd(), '../create/src/frameworks', frameworkId),
|
|
47
|
+
];
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
if (fs.existsSync(candidate)) {
|
|
50
|
+
return candidate;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return candidates[0];
|
|
54
|
+
}
|
|
55
|
+
async function startDevWatchMode(projectName, options) {
|
|
56
|
+
// Validate dev watch options
|
|
57
|
+
const validation = validateDevWatchOptions({ ...options, projectName });
|
|
58
|
+
if (!validation.valid) {
|
|
59
|
+
console.error(validation.error);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
// Enter dev watch mode
|
|
63
|
+
if (!projectName && !options.targetDir) {
|
|
64
|
+
console.error('Project name/target directory is required for dev watch mode');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
if (!options.framework) {
|
|
68
|
+
console.error('Failed to detect framework');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const framework = getFrameworkByName(options.framework);
|
|
72
|
+
if (!framework) {
|
|
73
|
+
console.error('Failed to detect framework');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
// First, create the app normally using the standard flow
|
|
77
|
+
const normalizedOpts = await normalizeOptions({
|
|
78
|
+
...options,
|
|
79
|
+
projectName,
|
|
80
|
+
framework: framework.id,
|
|
81
|
+
}, forcedAddOns);
|
|
82
|
+
if (!normalizedOpts) {
|
|
83
|
+
throw new Error('Failed to normalize options');
|
|
84
|
+
}
|
|
85
|
+
normalizedOpts.targetDir =
|
|
86
|
+
options.targetDir || resolve(process.cwd(), projectName);
|
|
87
|
+
// Create the initial app with minimal output for dev watch mode
|
|
88
|
+
console.log(chalk.bold('\ndev-watch'));
|
|
89
|
+
console.log(chalk.gray('├─') + ' ' + `creating initial ${appName} app...`);
|
|
90
|
+
if (normalizedOpts.install !== false) {
|
|
91
|
+
console.log(chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...');
|
|
92
|
+
}
|
|
93
|
+
const silentEnvironment = createUIEnvironment(appName, true);
|
|
94
|
+
await confirmTargetDirectorySafety(normalizedOpts.targetDir, options.force);
|
|
95
|
+
await createApp(silentEnvironment, normalizedOpts);
|
|
96
|
+
console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`);
|
|
97
|
+
// Now start the dev watch mode
|
|
98
|
+
const manager = new DevWatchManager({
|
|
99
|
+
watchPath: options.devWatch,
|
|
100
|
+
targetDir: normalizedOpts.targetDir,
|
|
101
|
+
framework,
|
|
102
|
+
cliOptions: normalizedOpts,
|
|
103
|
+
packageManager: normalizedOpts.packageManager,
|
|
104
|
+
runDevCommand: options.runDev,
|
|
105
|
+
environment,
|
|
106
|
+
frameworkDefinitionInitializers,
|
|
107
|
+
});
|
|
108
|
+
await manager.start();
|
|
109
|
+
}
|
|
44
110
|
const toolchains = new Set();
|
|
45
111
|
for (const framework of getFrameworks()) {
|
|
46
112
|
for (const addOn of framework.getAddOns()) {
|
|
@@ -142,58 +208,7 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
|
|
|
142
208
|
return;
|
|
143
209
|
}
|
|
144
210
|
if (options.devWatch) {
|
|
145
|
-
|
|
146
|
-
const validation = validateDevWatchOptions({ ...options, projectName });
|
|
147
|
-
if (!validation.valid) {
|
|
148
|
-
console.error(validation.error);
|
|
149
|
-
process.exit(1);
|
|
150
|
-
}
|
|
151
|
-
// Enter dev watch mode
|
|
152
|
-
if (!projectName && !options.targetDir) {
|
|
153
|
-
console.error('Project name/target directory is required for dev watch mode');
|
|
154
|
-
process.exit(1);
|
|
155
|
-
}
|
|
156
|
-
if (!options.framework) {
|
|
157
|
-
console.error('Failed to detect framework');
|
|
158
|
-
process.exit(1);
|
|
159
|
-
}
|
|
160
|
-
const framework = getFrameworkByName(options.framework);
|
|
161
|
-
if (!framework) {
|
|
162
|
-
console.error('Failed to detect framework');
|
|
163
|
-
process.exit(1);
|
|
164
|
-
}
|
|
165
|
-
// First, create the app normally using the standard flow
|
|
166
|
-
const normalizedOpts = await normalizeOptions({
|
|
167
|
-
...options,
|
|
168
|
-
projectName,
|
|
169
|
-
framework: framework.id,
|
|
170
|
-
}, forcedAddOns);
|
|
171
|
-
if (!normalizedOpts) {
|
|
172
|
-
throw new Error('Failed to normalize options');
|
|
173
|
-
}
|
|
174
|
-
normalizedOpts.targetDir =
|
|
175
|
-
options.targetDir || resolve(process.cwd(), projectName);
|
|
176
|
-
// Create the initial app with minimal output for dev watch mode
|
|
177
|
-
console.log(chalk.bold('\ndev-watch'));
|
|
178
|
-
console.log(chalk.gray('├─') + ' ' + `creating initial ${appName} app...`);
|
|
179
|
-
if (normalizedOpts.install !== false) {
|
|
180
|
-
console.log(chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...');
|
|
181
|
-
}
|
|
182
|
-
const silentEnvironment = createUIEnvironment(appName, true);
|
|
183
|
-
await confirmTargetDirectorySafety(normalizedOpts.targetDir, options.force);
|
|
184
|
-
await createApp(silentEnvironment, normalizedOpts);
|
|
185
|
-
console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`);
|
|
186
|
-
// Now start the dev watch mode
|
|
187
|
-
const manager = new DevWatchManager({
|
|
188
|
-
watchPath: options.devWatch,
|
|
189
|
-
targetDir: normalizedOpts.targetDir,
|
|
190
|
-
framework,
|
|
191
|
-
cliOptions: normalizedOpts,
|
|
192
|
-
packageManager: normalizedOpts.packageManager,
|
|
193
|
-
environment,
|
|
194
|
-
frameworkDefinitionInitializers,
|
|
195
|
-
});
|
|
196
|
-
await manager.start();
|
|
211
|
+
await startDevWatchMode(projectName, options);
|
|
197
212
|
return;
|
|
198
213
|
}
|
|
199
214
|
try {
|
|
@@ -206,6 +221,7 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
|
|
|
206
221
|
}
|
|
207
222
|
if (cliOptions.routerOnly !== true &&
|
|
208
223
|
cliOptions.template &&
|
|
224
|
+
['file-router', 'typescript', 'tsx', 'javascript', 'js', 'jsx'].includes(cliOptions.template.toLowerCase()) &&
|
|
209
225
|
cliOptions.template.toLowerCase() !== 'file-router') {
|
|
210
226
|
cliOptions.routerOnly = true;
|
|
211
227
|
}
|
|
@@ -217,24 +233,6 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
|
|
|
217
233
|
else {
|
|
218
234
|
finalOptions = await normalizeOptions(cliOptions, forcedAddOns, { forcedDeployment });
|
|
219
235
|
}
|
|
220
|
-
if (options.ui) {
|
|
221
|
-
const optionsFromCLI = await normalizeOptions(cliOptions, forcedAddOns, { disableNameCheck: true, forcedDeployment });
|
|
222
|
-
const uiOptions = {
|
|
223
|
-
...createSerializedOptions(optionsFromCLI),
|
|
224
|
-
projectName: 'my-app',
|
|
225
|
-
targetDir: resolve(process.cwd(), 'my-app'),
|
|
226
|
-
};
|
|
227
|
-
launchUI({
|
|
228
|
-
mode: 'setup',
|
|
229
|
-
options: uiOptions,
|
|
230
|
-
forcedRouterMode: defaultMode,
|
|
231
|
-
forcedAddOns,
|
|
232
|
-
environmentFactory: () => createUIEnvironment(appName, false),
|
|
233
|
-
webBase,
|
|
234
|
-
showDeploymentOptions,
|
|
235
|
-
});
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
236
|
if (finalOptions) {
|
|
239
237
|
intro(`Creating a new ${appName} app in ${projectName}...`);
|
|
240
238
|
}
|
|
@@ -281,6 +279,9 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
|
|
|
281
279
|
cmd.argument('[project-name]', 'name of the project');
|
|
282
280
|
if (!defaultFramework) {
|
|
283
281
|
cmd.option('--framework <type>', `project framework (${availableFrameworks.join(', ')})`, (value) => {
|
|
282
|
+
if (value.toLowerCase() === 'react-cra') {
|
|
283
|
+
return 'react';
|
|
284
|
+
}
|
|
284
285
|
if (!availableFrameworks.some((f) => f.toLowerCase() === value.toLowerCase())) {
|
|
285
286
|
throw new InvalidArgumentError(`Invalid framework: ${value}. Only the following are allowed: ${availableFrameworks.join(', ')}`);
|
|
286
287
|
}
|
|
@@ -288,7 +289,9 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
|
|
|
288
289
|
}, defaultFramework || 'React');
|
|
289
290
|
}
|
|
290
291
|
cmd
|
|
291
|
-
.option('--starter [url]', '
|
|
292
|
+
.option('--starter [url-or-id]', 'DEPRECATED: use --template. Initializes from a template URL or built-in id', false)
|
|
293
|
+
.option('--template-id <id>', 'initialize using a built-in template id')
|
|
294
|
+
.option('--template [url-or-id]', 'initialize this project from a template URL or built-in template id')
|
|
292
295
|
.option('--no-install', 'skip installing dependencies')
|
|
293
296
|
.option(`--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`, `Explicitly tell the CLI to use this package manager`, (value) => {
|
|
294
297
|
if (!SUPPORTED_PACKAGE_MANAGERS.includes(value)) {
|
|
@@ -297,8 +300,8 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
|
|
|
297
300
|
return value;
|
|
298
301
|
})
|
|
299
302
|
.option('--dev-watch <path>', 'Watch a framework directory for changes and auto-rebuild')
|
|
303
|
+
.option('--run-dev', 'Run the app dev server alongside dev-watch', false)
|
|
300
304
|
.option('--router-only', 'Use router-only compatibility mode (file-based routing without TanStack Start)')
|
|
301
|
-
.option('--template <type>', 'Deprecated: compatibility flag from create-tsrouter-app')
|
|
302
305
|
.option('--tailwind', 'Deprecated: compatibility flag; Tailwind is always enabled')
|
|
303
306
|
.option('--no-tailwind', 'Deprecated: compatibility flag; Tailwind opt-out is ignored')
|
|
304
307
|
.option('--examples', 'include demo/example pages')
|
|
@@ -335,7 +338,6 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
|
|
|
335
338
|
.option('--git', 'create a git repository')
|
|
336
339
|
.option('--no-git', 'do not create a git repository')
|
|
337
340
|
.option('--target-dir <path>', 'the target directory for the application root')
|
|
338
|
-
.option('--ui', 'Launch the UI for project creation')
|
|
339
341
|
.option('--add-on-config <config>', 'JSON string with add-on configuration options')
|
|
340
342
|
.option('-f, --force', 'force project creation even if the target directory is not empty', false);
|
|
341
343
|
return cmd;
|
|
@@ -347,6 +349,28 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
|
|
|
347
349
|
.description(`Create a new TanStack Start application`);
|
|
348
350
|
configureCreateCommand(createCommand);
|
|
349
351
|
createCommand.action(handleCreate);
|
|
352
|
+
// === DEV SUBCOMMAND ===
|
|
353
|
+
const devCommand = program
|
|
354
|
+
.command('dev')
|
|
355
|
+
.description('Create a sandbox app and watch built-in framework templates/add-ons');
|
|
356
|
+
configureCreateCommand(devCommand);
|
|
357
|
+
devCommand.action(async (projectName, options) => {
|
|
358
|
+
const frameworkName = options.framework || defaultFramework || 'React';
|
|
359
|
+
const framework = getFrameworkByName(frameworkName);
|
|
360
|
+
if (!framework) {
|
|
361
|
+
console.error(`Unknown framework: ${frameworkName}`);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
const watchPath = resolveBuiltInDevWatchPath(framework.id);
|
|
365
|
+
const devOptions = {
|
|
366
|
+
...options,
|
|
367
|
+
framework: framework.name,
|
|
368
|
+
devWatch: watchPath,
|
|
369
|
+
runDev: true,
|
|
370
|
+
install: options.install ?? true,
|
|
371
|
+
};
|
|
372
|
+
await startDevWatchMode(projectName, devOptions);
|
|
373
|
+
});
|
|
350
374
|
// === MCP SUBCOMMAND ===
|
|
351
375
|
program
|
|
352
376
|
.command('mcp')
|
|
@@ -424,7 +448,6 @@ Remove your node_modules directory and package lock file and re-install.`);
|
|
|
424
448
|
.command('add')
|
|
425
449
|
.argument('[add-on...]', 'Name of the add-ons (or add-ons separated by spaces or commas)')
|
|
426
450
|
.option('--forced', 'Force the add-on to be added', false)
|
|
427
|
-
.option('--ui', 'Add with the UI')
|
|
428
451
|
.action(async (addOns, options) => {
|
|
429
452
|
const parsedAddOns = [];
|
|
430
453
|
for (const addOn of addOns) {
|
|
@@ -435,19 +458,7 @@ Remove your node_modules directory and package lock file and re-install.`);
|
|
|
435
458
|
parsedAddOns.push(addOn.trim());
|
|
436
459
|
}
|
|
437
460
|
}
|
|
438
|
-
if (
|
|
439
|
-
launchUI({
|
|
440
|
-
mode: 'add',
|
|
441
|
-
addOns: parsedAddOns,
|
|
442
|
-
projectPath: resolve(process.cwd()),
|
|
443
|
-
forcedRouterMode: defaultMode,
|
|
444
|
-
forcedAddOns,
|
|
445
|
-
environmentFactory: () => createUIEnvironment(appName, false),
|
|
446
|
-
webBase,
|
|
447
|
-
showDeploymentOptions,
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
else if (parsedAddOns.length < 1) {
|
|
461
|
+
if (parsedAddOns.length < 1) {
|
|
451
462
|
const selectedAddOns = await promptForAddOns();
|
|
452
463
|
if (selectedAddOns.length) {
|
|
453
464
|
await addToApp(environment, selectedAddOns, resolve(process.cwd()), {
|
|
@@ -481,17 +492,31 @@ Remove your node_modules directory and package lock file and re-install.`);
|
|
|
481
492
|
.action(async () => {
|
|
482
493
|
await devAddOn(environment);
|
|
483
494
|
});
|
|
484
|
-
// ===
|
|
495
|
+
// === TEMPLATE SUBCOMMAND ===
|
|
496
|
+
const templateCommand = program.command('template');
|
|
497
|
+
templateCommand
|
|
498
|
+
.command('init')
|
|
499
|
+
.description('Initialize a project template from the current project')
|
|
500
|
+
.action(async () => {
|
|
501
|
+
await initStarter(environment);
|
|
502
|
+
});
|
|
503
|
+
templateCommand
|
|
504
|
+
.command('compile')
|
|
505
|
+
.description('Compile the template JSON file for the current project')
|
|
506
|
+
.action(async () => {
|
|
507
|
+
await compileStarter(environment);
|
|
508
|
+
});
|
|
509
|
+
// Legacy alias for template command
|
|
485
510
|
const starterCommand = program.command('starter');
|
|
486
511
|
starterCommand
|
|
487
512
|
.command('init')
|
|
488
|
-
.description('
|
|
513
|
+
.description('Deprecated alias: initialize a project template')
|
|
489
514
|
.action(async () => {
|
|
490
515
|
await initStarter(environment);
|
|
491
516
|
});
|
|
492
517
|
starterCommand
|
|
493
518
|
.command('compile')
|
|
494
|
-
.description('
|
|
519
|
+
.description('Deprecated alias: compile the template JSON file')
|
|
495
520
|
.action(async () => {
|
|
496
521
|
await compileStarter(environment);
|
|
497
522
|
});
|
package/dist/command-line.js
CHANGED
|
@@ -1,16 +1,136 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
-
import { DEFAULT_PACKAGE_MANAGER, finalizeAddOns, getFrameworkById, getPackageManager, loadStarter, populateAddOnOptionsDefaults, } from '@tanstack/create';
|
|
3
|
+
import { DEFAULT_PACKAGE_MANAGER, finalizeAddOns, getFrameworkById, getPackageManager, getRawRegistry, loadStarter, populateAddOnOptionsDefaults, } from '@tanstack/create';
|
|
4
4
|
import { getCurrentDirectoryName, sanitizePackageName, validateProjectName, } from './utils.js';
|
|
5
5
|
const SUPPORTED_LEGACY_TEMPLATES = new Set([
|
|
6
6
|
'file-router',
|
|
7
7
|
'typescript',
|
|
8
8
|
'tsx',
|
|
9
9
|
]);
|
|
10
|
+
const LEGACY_TEMPLATE_ALIASES = new Set(['javascript', 'js', 'jsx']);
|
|
11
|
+
function getLegacyTemplateValue(templateValue) {
|
|
12
|
+
if (!templateValue) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const normalized = templateValue.toLowerCase().trim();
|
|
16
|
+
if (SUPPORTED_LEGACY_TEMPLATES.has(normalized) ||
|
|
17
|
+
LEGACY_TEMPLATE_ALIASES.has(normalized)) {
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
function slugifyStarterName(value) {
|
|
23
|
+
return value
|
|
24
|
+
.trim()
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
27
|
+
.replace(/^-+|-+$/g, '');
|
|
28
|
+
}
|
|
29
|
+
function isLikelyStarterUrlOrPath(value) {
|
|
30
|
+
return (/^https?:\/\//i.test(value) ||
|
|
31
|
+
/^file:\/\//i.test(value) ||
|
|
32
|
+
value.startsWith('./') ||
|
|
33
|
+
value.startsWith('../') ||
|
|
34
|
+
value.startsWith('/') ||
|
|
35
|
+
/^[a-zA-Z]:[\\/]/.test(value));
|
|
36
|
+
}
|
|
37
|
+
function getStarterIdsFromUrl(starterUrl) {
|
|
38
|
+
const ids = new Set();
|
|
39
|
+
try {
|
|
40
|
+
const pathname = new URL(starterUrl).pathname;
|
|
41
|
+
const parts = pathname.split('/').filter(Boolean);
|
|
42
|
+
const lastPart = parts.at(-1)?.toLowerCase();
|
|
43
|
+
if (lastPart) {
|
|
44
|
+
ids.add(lastPart.replace(/\.json$/i, ''));
|
|
45
|
+
}
|
|
46
|
+
if ((lastPart === 'starter.json' || lastPart === 'template.json') &&
|
|
47
|
+
parts.length >= 2) {
|
|
48
|
+
ids.add(parts.at(-2).toLowerCase());
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Ignore URL parse errors and rely on other id heuristics.
|
|
53
|
+
}
|
|
54
|
+
return ids;
|
|
55
|
+
}
|
|
56
|
+
function resolveMonorepoStarterById(starterId) {
|
|
57
|
+
const normalized = starterId.toLowerCase().trim();
|
|
58
|
+
const idVariants = Array.from(new Set([
|
|
59
|
+
normalized,
|
|
60
|
+
normalized.replace(/-template$/i, ''),
|
|
61
|
+
normalized.replace(/-starter$/i, ''),
|
|
62
|
+
])).filter(Boolean);
|
|
63
|
+
const cwd = process.cwd();
|
|
64
|
+
const rootCandidates = [
|
|
65
|
+
cwd,
|
|
66
|
+
resolve(cwd, '..'),
|
|
67
|
+
resolve(cwd, '../..'),
|
|
68
|
+
resolve(cwd, '../../..'),
|
|
69
|
+
];
|
|
70
|
+
for (const root of rootCandidates) {
|
|
71
|
+
for (const framework of ['react', 'solid']) {
|
|
72
|
+
for (const id of idVariants) {
|
|
73
|
+
const templatePath = resolve(root, 'examples', framework, id, 'template.json');
|
|
74
|
+
if (fs.existsSync(templatePath)) {
|
|
75
|
+
return templatePath;
|
|
76
|
+
}
|
|
77
|
+
const starterPath = resolve(root, 'examples', framework, id, 'starter.json');
|
|
78
|
+
if (fs.existsSync(starterPath)) {
|
|
79
|
+
return starterPath;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
async function resolveStarterSpecifier(starterSpecifier) {
|
|
87
|
+
const normalized = starterSpecifier.trim();
|
|
88
|
+
if (!normalized || isLikelyStarterUrlOrPath(normalized)) {
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
91
|
+
const registry = await getRawRegistry();
|
|
92
|
+
if (registry && registry.starters?.length) {
|
|
93
|
+
const lookup = normalized.toLowerCase();
|
|
94
|
+
const match = registry.starters.find((starter) => {
|
|
95
|
+
const candidateIds = new Set();
|
|
96
|
+
candidateIds.add(starter.name.toLowerCase());
|
|
97
|
+
candidateIds.add(slugifyStarterName(starter.name));
|
|
98
|
+
for (const id of getStarterIdsFromUrl(starter.url)) {
|
|
99
|
+
candidateIds.add(id);
|
|
100
|
+
}
|
|
101
|
+
return candidateIds.has(lookup);
|
|
102
|
+
});
|
|
103
|
+
if (match) {
|
|
104
|
+
return match.url;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const monorepoStarterPath = resolveMonorepoStarterById(normalized);
|
|
108
|
+
if (monorepoStarterPath) {
|
|
109
|
+
return monorepoStarterPath;
|
|
110
|
+
}
|
|
111
|
+
if (!registry || !registry.starters?.length) {
|
|
112
|
+
throw new Error(`Could not resolve template id "${normalized}" because no template registry is configured. Pass a template URL (or set CTA_REGISTRY).`);
|
|
113
|
+
}
|
|
114
|
+
const availableIds = Array.from(new Set(registry.starters.flatMap((starter) => {
|
|
115
|
+
const ids = [slugifyStarterName(starter.name)];
|
|
116
|
+
ids.push(...Array.from(getStarterIdsFromUrl(starter.url)));
|
|
117
|
+
return ids;
|
|
118
|
+
})))
|
|
119
|
+
.filter(Boolean)
|
|
120
|
+
.sort();
|
|
121
|
+
throw new Error(`Unknown template id "${normalized}". Available built-in templates: ${availableIds.join(', ')}`);
|
|
122
|
+
}
|
|
10
123
|
export function validateLegacyCreateFlags(cliOptions) {
|
|
11
124
|
const warnings = [];
|
|
125
|
+
const legacyTemplate = getLegacyTemplateValue(cliOptions.template);
|
|
126
|
+
if (cliOptions.starter) {
|
|
127
|
+
warnings.push('The --starter flag is deprecated; prefer --template instead. Backward compatibility remains for now.');
|
|
128
|
+
}
|
|
129
|
+
if (cliOptions.starter && cliOptions.template && !legacyTemplate) {
|
|
130
|
+
warnings.push('Both --starter and --template were provided. --template takes precedence.');
|
|
131
|
+
}
|
|
12
132
|
if (cliOptions.routerOnly) {
|
|
13
|
-
warnings.push('The --router-only flag enables router-only compatibility mode. Start-dependent add-ons, deployment adapters, and
|
|
133
|
+
warnings.push('The --router-only flag enables router-only compatibility mode. Start-dependent add-ons, deployment adapters, and templates are disabled; only the base template and optional toolchain are supported.');
|
|
14
134
|
}
|
|
15
135
|
if (cliOptions.routerOnly && cliOptions.addOns) {
|
|
16
136
|
warnings.push('Ignoring --add-ons in router-only compatibility mode.');
|
|
@@ -19,7 +139,10 @@ export function validateLegacyCreateFlags(cliOptions) {
|
|
|
19
139
|
warnings.push('Ignoring --deployment in router-only compatibility mode.');
|
|
20
140
|
}
|
|
21
141
|
if (cliOptions.routerOnly && cliOptions.starter) {
|
|
22
|
-
warnings.push('Ignoring --starter in router-only compatibility mode.');
|
|
142
|
+
warnings.push('Ignoring --starter/--template in router-only compatibility mode.');
|
|
143
|
+
}
|
|
144
|
+
if (cliOptions.routerOnly && cliOptions.templateId) {
|
|
145
|
+
warnings.push('Ignoring --template-id in router-only compatibility mode.');
|
|
23
146
|
}
|
|
24
147
|
if (cliOptions.tailwind === true) {
|
|
25
148
|
warnings.push('The --tailwind flag is deprecated and ignored. Tailwind is always enabled in TanStack Start scaffolds.');
|
|
@@ -27,10 +150,10 @@ export function validateLegacyCreateFlags(cliOptions) {
|
|
|
27
150
|
if (cliOptions.tailwind === false) {
|
|
28
151
|
warnings.push('The --no-tailwind flag is deprecated and ignored. Tailwind opt-out is intentionally unsupported to keep add-on permutations maintainable; remove Tailwind after scaffolding if needed.');
|
|
29
152
|
}
|
|
30
|
-
if (!
|
|
153
|
+
if (!legacyTemplate) {
|
|
31
154
|
return { warnings };
|
|
32
155
|
}
|
|
33
|
-
const template =
|
|
156
|
+
const template = legacyTemplate;
|
|
34
157
|
if (template === 'javascript' || template === 'js' || template === 'jsx') {
|
|
35
158
|
return {
|
|
36
159
|
warnings,
|
|
@@ -70,12 +193,24 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
|
|
|
70
193
|
// Mode is always file-router (TanStack Start)
|
|
71
194
|
let mode = 'file-router';
|
|
72
195
|
let routerOnly = !!cliOptions.routerOnly;
|
|
73
|
-
const
|
|
196
|
+
const legacyTemplate = getLegacyTemplateValue(cliOptions.template);
|
|
197
|
+
if (!cliOptions.starter) {
|
|
198
|
+
if (cliOptions.template && !legacyTemplate) {
|
|
199
|
+
cliOptions.starter = cliOptions.template;
|
|
200
|
+
}
|
|
201
|
+
else if (cliOptions.templateId) {
|
|
202
|
+
cliOptions.starter = cliOptions.templateId;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const template = legacyTemplate;
|
|
74
206
|
if (template && template !== 'file-router') {
|
|
75
207
|
routerOnly = true;
|
|
76
208
|
}
|
|
209
|
+
if (!cliOptions.starter && cliOptions.templateId) {
|
|
210
|
+
cliOptions.starter = cliOptions.templateId;
|
|
211
|
+
}
|
|
77
212
|
const starter = !routerOnly && cliOptions.starter
|
|
78
|
-
? await loadStarter(cliOptions.starter)
|
|
213
|
+
? await loadStarter(await resolveStarterSpecifier(cliOptions.starter))
|
|
79
214
|
: undefined;
|
|
80
215
|
// TypeScript and Tailwind are always enabled with TanStack Start
|
|
81
216
|
const typescript = true;
|
|
@@ -84,7 +219,7 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
|
|
|
84
219
|
cliOptions.framework = starter.framework;
|
|
85
220
|
mode = starter.mode;
|
|
86
221
|
}
|
|
87
|
-
const framework = getFrameworkById(cliOptions.framework || 'react
|
|
222
|
+
const framework = getFrameworkById(cliOptions.framework || 'react');
|
|
88
223
|
async function selectAddOns() {
|
|
89
224
|
// Edge case for Windows Powershell
|
|
90
225
|
if (Array.isArray(cliOptions.addOns) && cliOptions.addOns.length === 1) {
|