@tanstack/cli 0.0.8 → 0.48.3
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/LICENSE +21 -0
- package/dist/bin.js +7 -0
- package/dist/cli.js +481 -0
- package/dist/command-line.js +174 -0
- package/dist/dev-watch.js +290 -0
- package/dist/file-syncer.js +148 -0
- package/dist/index.js +1 -0
- package/dist/mcp/api.js +31 -0
- package/dist/mcp/tools.js +250 -0
- package/dist/mcp/types.js +37 -0
- package/dist/mcp.js +121 -0
- package/dist/options.js +162 -0
- package/dist/types/bin.d.ts +2 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/command-line.d.ts +10 -0
- package/dist/types/dev-watch.d.ts +27 -0
- package/dist/types/file-syncer.d.ts +18 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mcp/api.d.ts +4 -0
- package/dist/types/mcp/tools.d.ts +2 -0
- package/dist/types/mcp/types.d.ts +217 -0
- package/dist/types/mcp.d.ts +6 -0
- package/dist/types/options.d.ts +8 -0
- package/dist/types/types.d.ts +25 -0
- package/dist/types/ui-environment.d.ts +2 -0
- package/dist/types/ui-prompts.d.ts +12 -0
- package/dist/types/utils.d.ts +8 -0
- package/dist/types.js +1 -0
- package/dist/ui-environment.js +52 -0
- package/dist/ui-prompts.js +244 -0
- package/dist/utils.js +30 -0
- package/package.json +46 -47
- package/src/bin.ts +6 -93
- package/src/cli.ts +692 -0
- package/src/command-line.ts +236 -0
- package/src/dev-watch.ts +430 -0
- package/src/file-syncer.ts +205 -0
- package/src/index.ts +1 -85
- package/src/mcp.ts +190 -0
- package/src/options.ts +260 -0
- package/src/types.ts +27 -0
- package/src/ui-environment.ts +74 -0
- package/src/ui-prompts.ts +322 -0
- package/src/utils.ts +38 -0
- package/tests/command-line.test.ts +304 -0
- package/tests/index.test.ts +9 -0
- package/tests/mcp.test.ts +225 -0
- package/tests/options.test.ts +304 -0
- package/tests/setupVitest.ts +6 -0
- package/tests/ui-environment.test.ts +97 -0
- package/tests/ui-prompts.test.ts +238 -0
- package/tsconfig.json +17 -0
- package/vitest.config.js +7 -0
- package/dist/bin.cjs +0 -769
- package/dist/bin.d.cts +0 -1
- package/dist/bin.d.mts +0 -1
- package/dist/bin.mjs +0 -768
- package/dist/fetch-CbFFGJEw.cjs +0 -3
- package/dist/fetch-DG5dLrsb.cjs +0 -522
- package/dist/fetch-DhlVXS6S.mjs +0 -390
- package/dist/fetch-I_OVg8JX.mjs +0 -3
- package/dist/index.cjs +0 -37
- package/dist/index.d.cts +0 -1172
- package/dist/index.d.mts +0 -1172
- package/dist/index.mjs +0 -4
- package/dist/template-Szi7-AZJ.mjs +0 -2202
- package/dist/template-lWrIZhCQ.cjs +0 -2314
- package/src/api/fetch.test.ts +0 -114
- package/src/api/fetch.ts +0 -278
- package/src/cache/index.ts +0 -89
- package/src/commands/create.ts +0 -470
- package/src/commands/mcp.test.ts +0 -152
- package/src/commands/mcp.ts +0 -211
- package/src/engine/compile-with-addons.test.ts +0 -302
- package/src/engine/compile.test.ts +0 -404
- package/src/engine/compile.ts +0 -569
- package/src/engine/config-file.test.ts +0 -118
- package/src/engine/config-file.ts +0 -61
- package/src/engine/custom-addons/integration.ts +0 -323
- package/src/engine/custom-addons/shared.test.ts +0 -98
- package/src/engine/custom-addons/shared.ts +0 -281
- package/src/engine/custom-addons/template.test.ts +0 -288
- package/src/engine/custom-addons/template.ts +0 -124
- package/src/engine/template.test.ts +0 -256
- package/src/engine/template.ts +0 -269
- package/src/engine/types.ts +0 -336
- package/src/parse-gitignore.d.ts +0 -5
- package/src/templates/base.ts +0 -883
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021-present Tanner Linsley
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/bin.js
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { Command, InvalidArgumentError } from 'commander';
|
|
4
|
+
import { intro, log } from '@clack/prompts';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import semver from 'semver';
|
|
7
|
+
import { SUPPORTED_PACKAGE_MANAGERS, addToApp, compileAddOn, compileStarter, createApp, createSerializedOptions, getAllAddOns, getFrameworkByName, getFrameworks, initAddOn, initStarter, } from '@tanstack/create';
|
|
8
|
+
import { launchUI } from '@tanstack/create-ui';
|
|
9
|
+
import { runMCPServer } from './mcp.js';
|
|
10
|
+
import { promptForAddOns, promptForCreateOptions } from './options.js';
|
|
11
|
+
import { normalizeOptions, validateDevWatchOptions } from './command-line.js';
|
|
12
|
+
import { createUIEnvironment } from './ui-environment.js';
|
|
13
|
+
import { convertTemplateToMode } from './utils.js';
|
|
14
|
+
import { DevWatchManager } from './dev-watch.js';
|
|
15
|
+
// Read version from package.json
|
|
16
|
+
const packageJsonPath = new URL('../package.json', import.meta.url);
|
|
17
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
18
|
+
const VERSION = packageJson.version;
|
|
19
|
+
export function cli({ name, appName, forcedMode, forcedAddOns = [], defaultTemplate = 'javascript', forcedDeployment, defaultFramework, craCompatible = false, webBase, frameworkDefinitionInitializers, showDeploymentOptions = false, legacyAutoCreate = false, }) {
|
|
20
|
+
const environment = createUIEnvironment(appName, false);
|
|
21
|
+
const program = new Command();
|
|
22
|
+
const availableFrameworks = getFrameworks().map((f) => f.name);
|
|
23
|
+
const toolchains = new Set();
|
|
24
|
+
for (const framework of getFrameworks()) {
|
|
25
|
+
for (const addOn of framework.getAddOns()) {
|
|
26
|
+
if (addOn.type === 'toolchain') {
|
|
27
|
+
toolchains.add(addOn.id);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const deployments = new Set();
|
|
32
|
+
for (const framework of getFrameworks()) {
|
|
33
|
+
for (const addOn of framework.getAddOns()) {
|
|
34
|
+
if (addOn.type === 'deployment') {
|
|
35
|
+
deployments.add(addOn.id);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
let defaultMode = forcedMode;
|
|
40
|
+
const supportedModes = new Set();
|
|
41
|
+
for (const framework of getFrameworks()) {
|
|
42
|
+
for (const mode of Object.keys(framework.supportedModes)) {
|
|
43
|
+
supportedModes.add(mode);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (defaultMode && !supportedModes.has(defaultMode)) {
|
|
47
|
+
throw new InvalidArgumentError(`Invalid mode: ${defaultMode}. The following are allowed: ${Array.from(supportedModes).join(', ')}`);
|
|
48
|
+
}
|
|
49
|
+
if (supportedModes.size < 2) {
|
|
50
|
+
defaultMode = Array.from(supportedModes)[0];
|
|
51
|
+
}
|
|
52
|
+
program
|
|
53
|
+
.name(name)
|
|
54
|
+
.description(`${appName} CLI`)
|
|
55
|
+
.version(VERSION, '-v, --version', 'output the current version');
|
|
56
|
+
// Helper to create the create command action handler
|
|
57
|
+
async function handleCreate(projectName, options) {
|
|
58
|
+
if (options.listAddOns) {
|
|
59
|
+
const addOns = await getAllAddOns(getFrameworkByName(options.framework || defaultFramework || 'React'), defaultMode ||
|
|
60
|
+
convertTemplateToMode(options.template || defaultTemplate));
|
|
61
|
+
let hasConfigurableAddOns = false;
|
|
62
|
+
for (const addOn of addOns.filter((a) => !forcedAddOns.includes(a.id))) {
|
|
63
|
+
const hasOptions = addOn.options && Object.keys(addOn.options).length > 0;
|
|
64
|
+
const optionMarker = hasOptions ? '*' : ' ';
|
|
65
|
+
if (hasOptions)
|
|
66
|
+
hasConfigurableAddOns = true;
|
|
67
|
+
console.log(`${optionMarker} ${chalk.bold(addOn.id)}: ${addOn.description}`);
|
|
68
|
+
}
|
|
69
|
+
if (hasConfigurableAddOns) {
|
|
70
|
+
console.log('\n* = has configuration options');
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (options.addonDetails) {
|
|
75
|
+
const addOns = await getAllAddOns(getFrameworkByName(options.framework || defaultFramework || 'React'), defaultMode ||
|
|
76
|
+
convertTemplateToMode(options.template || defaultTemplate));
|
|
77
|
+
const addOn = addOns.find((a) => a.id === options.addonDetails);
|
|
78
|
+
if (!addOn) {
|
|
79
|
+
console.error(`Add-on '${options.addonDetails}' not found`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
console.log(`${chalk.bold.cyan('Add-on Details:')} ${chalk.bold(addOn.name)}`);
|
|
83
|
+
console.log(`${chalk.bold('ID:')} ${addOn.id}`);
|
|
84
|
+
console.log(`${chalk.bold('Description:')} ${addOn.description}`);
|
|
85
|
+
console.log(`${chalk.bold('Type:')} ${addOn.type}`);
|
|
86
|
+
console.log(`${chalk.bold('Phase:')} ${addOn.phase}`);
|
|
87
|
+
console.log(`${chalk.bold('Supported Modes:')} ${addOn.modes.join(', ')}`);
|
|
88
|
+
if (addOn.link) {
|
|
89
|
+
console.log(`${chalk.bold('Link:')} ${chalk.blue(addOn.link)}`);
|
|
90
|
+
}
|
|
91
|
+
if (addOn.dependsOn && addOn.dependsOn.length > 0) {
|
|
92
|
+
console.log(`${chalk.bold('Dependencies:')} ${addOn.dependsOn.join(', ')}`);
|
|
93
|
+
}
|
|
94
|
+
if (addOn.options && Object.keys(addOn.options).length > 0) {
|
|
95
|
+
console.log(`\n${chalk.bold.yellow('Configuration Options:')}`);
|
|
96
|
+
for (const [optionName, option] of Object.entries(addOn.options)) {
|
|
97
|
+
if (option && typeof option === 'object' && 'type' in option) {
|
|
98
|
+
const opt = option;
|
|
99
|
+
console.log(` ${chalk.bold(optionName)}:`);
|
|
100
|
+
console.log(` Label: ${opt.label}`);
|
|
101
|
+
if (opt.description) {
|
|
102
|
+
console.log(` Description: ${opt.description}`);
|
|
103
|
+
}
|
|
104
|
+
console.log(` Type: ${opt.type}`);
|
|
105
|
+
console.log(` Default: ${opt.default}`);
|
|
106
|
+
if (opt.type === 'select' && opt.options) {
|
|
107
|
+
console.log(` Available values:`);
|
|
108
|
+
for (const choice of opt.options) {
|
|
109
|
+
console.log(` - ${choice.value}: ${choice.label}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.log(`\n${chalk.gray('No configuration options available')}`);
|
|
117
|
+
}
|
|
118
|
+
if (addOn.routes && addOn.routes.length > 0) {
|
|
119
|
+
console.log(`\n${chalk.bold.green('Routes:')}`);
|
|
120
|
+
for (const route of addOn.routes) {
|
|
121
|
+
console.log(` ${chalk.bold(route.url)} (${route.name})`);
|
|
122
|
+
console.log(` File: ${route.path}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (options.devWatch) {
|
|
128
|
+
// Validate dev watch options
|
|
129
|
+
const validation = validateDevWatchOptions({ ...options, projectName });
|
|
130
|
+
if (!validation.valid) {
|
|
131
|
+
console.error(validation.error);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
// Enter dev watch mode
|
|
135
|
+
if (!projectName && !options.targetDir) {
|
|
136
|
+
console.error('Project name/target directory is required for dev watch mode');
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
if (!options.framework) {
|
|
140
|
+
console.error('Failed to detect framework');
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const framework = getFrameworkByName(options.framework);
|
|
144
|
+
if (!framework) {
|
|
145
|
+
console.error('Failed to detect framework');
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
// First, create the app normally using the standard flow
|
|
149
|
+
const normalizedOpts = await normalizeOptions({
|
|
150
|
+
...options,
|
|
151
|
+
projectName,
|
|
152
|
+
framework: framework.id,
|
|
153
|
+
}, defaultMode, forcedAddOns);
|
|
154
|
+
if (!normalizedOpts) {
|
|
155
|
+
throw new Error('Failed to normalize options');
|
|
156
|
+
}
|
|
157
|
+
normalizedOpts.targetDir =
|
|
158
|
+
options.targetDir || resolve(process.cwd(), projectName);
|
|
159
|
+
// Create the initial app with minimal output for dev watch mode
|
|
160
|
+
console.log(chalk.bold('\ndev-watch'));
|
|
161
|
+
console.log(chalk.gray('├─') + ' ' + `creating initial ${appName} app...`);
|
|
162
|
+
if (normalizedOpts.install !== false) {
|
|
163
|
+
console.log(chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...');
|
|
164
|
+
}
|
|
165
|
+
const silentEnvironment = createUIEnvironment(appName, true);
|
|
166
|
+
await createApp(silentEnvironment, normalizedOpts);
|
|
167
|
+
console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`);
|
|
168
|
+
// Now start the dev watch mode
|
|
169
|
+
const manager = new DevWatchManager({
|
|
170
|
+
watchPath: options.devWatch,
|
|
171
|
+
targetDir: normalizedOpts.targetDir,
|
|
172
|
+
framework,
|
|
173
|
+
cliOptions: normalizedOpts,
|
|
174
|
+
packageManager: normalizedOpts.packageManager,
|
|
175
|
+
environment,
|
|
176
|
+
frameworkDefinitionInitializers,
|
|
177
|
+
});
|
|
178
|
+
await manager.start();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const cliOptions = {
|
|
183
|
+
projectName,
|
|
184
|
+
...options,
|
|
185
|
+
};
|
|
186
|
+
cliOptions.framework = getFrameworkByName(options.framework || defaultFramework || 'React').id;
|
|
187
|
+
if (defaultMode) {
|
|
188
|
+
cliOptions.template = defaultMode;
|
|
189
|
+
}
|
|
190
|
+
let finalOptions;
|
|
191
|
+
if (cliOptions.interactive) {
|
|
192
|
+
cliOptions.addOns = true;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
finalOptions = await normalizeOptions(cliOptions, defaultMode, forcedAddOns, { forcedDeployment });
|
|
196
|
+
}
|
|
197
|
+
if (options.ui) {
|
|
198
|
+
const optionsFromCLI = await normalizeOptions(cliOptions, defaultMode, forcedAddOns, { disableNameCheck: true, forcedDeployment });
|
|
199
|
+
const uiOptions = {
|
|
200
|
+
...createSerializedOptions(optionsFromCLI),
|
|
201
|
+
projectName: 'my-app',
|
|
202
|
+
targetDir: resolve(process.cwd(), 'my-app'),
|
|
203
|
+
};
|
|
204
|
+
launchUI({
|
|
205
|
+
mode: 'setup',
|
|
206
|
+
options: uiOptions,
|
|
207
|
+
forcedRouterMode: defaultMode,
|
|
208
|
+
forcedAddOns,
|
|
209
|
+
environmentFactory: () => createUIEnvironment(appName, false),
|
|
210
|
+
webBase,
|
|
211
|
+
showDeploymentOptions,
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (finalOptions) {
|
|
216
|
+
intro(`Creating a new ${appName} app in ${projectName}...`);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
intro(`Let's configure your ${appName} application`);
|
|
220
|
+
finalOptions = await promptForCreateOptions(cliOptions, {
|
|
221
|
+
forcedMode: defaultMode,
|
|
222
|
+
forcedAddOns,
|
|
223
|
+
showDeploymentOptions,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (!finalOptions) {
|
|
227
|
+
throw new Error('No options were provided');
|
|
228
|
+
}
|
|
229
|
+
// Determine target directory:
|
|
230
|
+
// 1. Use --target-dir if provided
|
|
231
|
+
// 2. Use targetDir from normalizeOptions if set (handles "." case)
|
|
232
|
+
// 3. If original projectName was ".", use current directory
|
|
233
|
+
// 4. Otherwise, use project name as subdirectory
|
|
234
|
+
if (options.targetDir) {
|
|
235
|
+
finalOptions.targetDir = options.targetDir;
|
|
236
|
+
}
|
|
237
|
+
else if (finalOptions.targetDir) {
|
|
238
|
+
// Keep the targetDir from normalizeOptions (handles "." case)
|
|
239
|
+
}
|
|
240
|
+
else if (projectName === '.') {
|
|
241
|
+
finalOptions.targetDir = resolve(process.cwd());
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
finalOptions.targetDir = resolve(process.cwd(), finalOptions.projectName);
|
|
245
|
+
}
|
|
246
|
+
await createApp(environment, finalOptions);
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
log.error(error instanceof Error ? error.message : 'An unknown error occurred');
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Helper to configure create command options
|
|
254
|
+
function configureCreateCommand(cmd) {
|
|
255
|
+
cmd.argument('[project-name]', 'name of the project');
|
|
256
|
+
if (!defaultMode && craCompatible) {
|
|
257
|
+
cmd.option('--template <type>', 'project template (typescript, javascript, file-router)', (value) => {
|
|
258
|
+
if (value !== 'typescript' &&
|
|
259
|
+
value !== 'javascript' &&
|
|
260
|
+
value !== 'file-router') {
|
|
261
|
+
throw new InvalidArgumentError(`Invalid template: ${value}. Only the following are allowed: typescript, javascript, file-router`);
|
|
262
|
+
}
|
|
263
|
+
return value;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
if (!defaultFramework) {
|
|
267
|
+
cmd.option('--framework <type>', `project framework (${availableFrameworks.join(', ')})`, (value) => {
|
|
268
|
+
if (!availableFrameworks.some((f) => f.toLowerCase() === value.toLowerCase())) {
|
|
269
|
+
throw new InvalidArgumentError(`Invalid framework: ${value}. Only the following are allowed: ${availableFrameworks.join(', ')}`);
|
|
270
|
+
}
|
|
271
|
+
return value;
|
|
272
|
+
}, defaultFramework || 'React');
|
|
273
|
+
}
|
|
274
|
+
cmd
|
|
275
|
+
.option('--starter [url]', 'initialize this project from a starter URL', false)
|
|
276
|
+
.option('--no-install', 'skip installing dependencies')
|
|
277
|
+
.option(`--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`, `Explicitly tell the CLI to use this package manager`, (value) => {
|
|
278
|
+
if (!SUPPORTED_PACKAGE_MANAGERS.includes(value)) {
|
|
279
|
+
throw new InvalidArgumentError(`Invalid package manager: ${value}. The following are allowed: ${SUPPORTED_PACKAGE_MANAGERS.join(', ')}`);
|
|
280
|
+
}
|
|
281
|
+
return value;
|
|
282
|
+
})
|
|
283
|
+
.option('--dev-watch <path>', 'Watch a framework directory for changes and auto-rebuild');
|
|
284
|
+
if (deployments.size > 0) {
|
|
285
|
+
cmd.option(`--deployment <${Array.from(deployments).join('|')}>`, `Explicitly tell the CLI to use this deployment adapter`, (value) => {
|
|
286
|
+
if (!deployments.has(value)) {
|
|
287
|
+
throw new InvalidArgumentError(`Invalid adapter: ${value}. The following are allowed: ${Array.from(deployments).join(', ')}`);
|
|
288
|
+
}
|
|
289
|
+
return value;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (toolchains.size > 0) {
|
|
293
|
+
cmd
|
|
294
|
+
.option(`--toolchain <${Array.from(toolchains).join('|')}>`, `Explicitly tell the CLI to use this toolchain`, (value) => {
|
|
295
|
+
if (!toolchains.has(value)) {
|
|
296
|
+
throw new InvalidArgumentError(`Invalid toolchain: ${value}. The following are allowed: ${Array.from(toolchains).join(', ')}`);
|
|
297
|
+
}
|
|
298
|
+
return value;
|
|
299
|
+
})
|
|
300
|
+
.option('--no-toolchain', 'skip toolchain selection');
|
|
301
|
+
}
|
|
302
|
+
cmd
|
|
303
|
+
.option('--interactive', 'interactive mode', false)
|
|
304
|
+
.option('--tailwind', 'add Tailwind CSS')
|
|
305
|
+
.option('--no-tailwind', 'skip Tailwind CSS')
|
|
306
|
+
.option('--add-ons [...add-ons]', 'pick from a list of available add-ons (comma separated list)', (value) => {
|
|
307
|
+
let addOns = !!value;
|
|
308
|
+
if (typeof value === 'string') {
|
|
309
|
+
addOns = value.split(',').map((addon) => addon.trim());
|
|
310
|
+
}
|
|
311
|
+
return addOns;
|
|
312
|
+
})
|
|
313
|
+
.option('--list-add-ons', 'list all available add-ons', false)
|
|
314
|
+
.option('--addon-details <addon-id>', 'show detailed information about a specific add-on')
|
|
315
|
+
.option('--no-git', 'do not create a git repository')
|
|
316
|
+
.option('--target-dir <path>', 'the target directory for the application root')
|
|
317
|
+
.option('--ui', 'Launch the UI for project creation')
|
|
318
|
+
.option('--add-on-config <config>', 'JSON string with add-on configuration options')
|
|
319
|
+
.option('-f, --force', 'force project creation even if the target directory is not empty', false);
|
|
320
|
+
return cmd;
|
|
321
|
+
}
|
|
322
|
+
// === CREATE SUBCOMMAND ===
|
|
323
|
+
const createCommand = program
|
|
324
|
+
.command('create')
|
|
325
|
+
.description(`Create a new ${appName} application`);
|
|
326
|
+
configureCreateCommand(createCommand);
|
|
327
|
+
createCommand.action(handleCreate);
|
|
328
|
+
// === MCP SUBCOMMAND ===
|
|
329
|
+
program
|
|
330
|
+
.command('mcp')
|
|
331
|
+
.description('Run the MCP (Model Context Protocol) server')
|
|
332
|
+
.option('--sse', 'Run in SSE mode instead of stdio', false)
|
|
333
|
+
.action(async (options) => {
|
|
334
|
+
await runMCPServer(options.sse, {
|
|
335
|
+
forcedMode: defaultMode,
|
|
336
|
+
forcedAddOns,
|
|
337
|
+
appName,
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
// === PIN-VERSIONS SUBCOMMAND ===
|
|
341
|
+
program
|
|
342
|
+
.command('pin-versions')
|
|
343
|
+
.description('Pin versions of the TanStack libraries')
|
|
344
|
+
.action(async () => {
|
|
345
|
+
if (!fs.existsSync('package.json')) {
|
|
346
|
+
console.error('package.json not found');
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
350
|
+
const packages = {
|
|
351
|
+
'@tanstack/react-router': '',
|
|
352
|
+
'@tanstack/router-generator': '',
|
|
353
|
+
'@tanstack/react-router-devtools': '',
|
|
354
|
+
'@tanstack/react-start': '',
|
|
355
|
+
'@tanstack/react-start-config': '',
|
|
356
|
+
'@tanstack/router-plugin': '',
|
|
357
|
+
'@tanstack/react-start-client': '',
|
|
358
|
+
'@tanstack/react-start-plugin': '1.115.0',
|
|
359
|
+
'@tanstack/react-start-server': '',
|
|
360
|
+
'@tanstack/start-server-core': '1.115.0',
|
|
361
|
+
};
|
|
362
|
+
function sortObject(obj) {
|
|
363
|
+
return Object.keys(obj)
|
|
364
|
+
.sort()
|
|
365
|
+
.reduce((acc, key) => {
|
|
366
|
+
acc[key] = obj[key];
|
|
367
|
+
return acc;
|
|
368
|
+
}, {});
|
|
369
|
+
}
|
|
370
|
+
if (!packageJson.dependencies['@tanstack/react-start']) {
|
|
371
|
+
console.error('@tanstack/react-start not found in dependencies');
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
let changed = 0;
|
|
375
|
+
const startVersion = packageJson.dependencies['@tanstack/react-start'].replace(/^\^/, '');
|
|
376
|
+
for (const pkg of Object.keys(packages)) {
|
|
377
|
+
if (!packageJson.dependencies[pkg]) {
|
|
378
|
+
packageJson.dependencies[pkg] = packages[pkg].length
|
|
379
|
+
? semver.maxSatisfying([startVersion, packages[pkg]], `^${packages[pkg]}`)
|
|
380
|
+
: startVersion;
|
|
381
|
+
changed++;
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
if (packageJson.dependencies[pkg].startsWith('^')) {
|
|
385
|
+
packageJson.dependencies[pkg] = packageJson.dependencies[pkg].replace(/^\^/, '');
|
|
386
|
+
changed++;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
packageJson.dependencies = sortObject(packageJson.dependencies);
|
|
391
|
+
if (changed > 0) {
|
|
392
|
+
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2));
|
|
393
|
+
console.log(`${changed} packages updated.
|
|
394
|
+
|
|
395
|
+
Remove your node_modules directory and package lock file and re-install.`);
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
console.log('No changes needed. The relevant TanStack packages are already pinned.');
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
// === ADD SUBCOMMAND ===
|
|
402
|
+
program
|
|
403
|
+
.command('add')
|
|
404
|
+
.argument('[add-on...]', 'Name of the add-ons (or add-ons separated by spaces or commas)')
|
|
405
|
+
.option('--forced', 'Force the add-on to be added', false)
|
|
406
|
+
.option('--ui', 'Add with the UI')
|
|
407
|
+
.action(async (addOns, options) => {
|
|
408
|
+
const parsedAddOns = [];
|
|
409
|
+
for (const addOn of addOns) {
|
|
410
|
+
if (addOn.includes(',') || addOn.includes(' ')) {
|
|
411
|
+
parsedAddOns.push(...addOn.split(/[\s,]+/).map((addon) => addon.trim()));
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
parsedAddOns.push(addOn.trim());
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (options.ui) {
|
|
418
|
+
launchUI({
|
|
419
|
+
mode: 'add',
|
|
420
|
+
addOns: parsedAddOns,
|
|
421
|
+
projectPath: resolve(process.cwd()),
|
|
422
|
+
forcedRouterMode: defaultMode,
|
|
423
|
+
forcedAddOns,
|
|
424
|
+
environmentFactory: () => createUIEnvironment(appName, false),
|
|
425
|
+
webBase,
|
|
426
|
+
showDeploymentOptions,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
else if (parsedAddOns.length < 1) {
|
|
430
|
+
const selectedAddOns = await promptForAddOns();
|
|
431
|
+
if (selectedAddOns.length) {
|
|
432
|
+
await addToApp(environment, selectedAddOns, resolve(process.cwd()), {
|
|
433
|
+
forced: options.forced,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
await addToApp(environment, parsedAddOns, resolve(process.cwd()), {
|
|
439
|
+
forced: options.forced,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
// === ADD-ON SUBCOMMAND ===
|
|
444
|
+
const addOnCommand = program.command('add-on');
|
|
445
|
+
addOnCommand
|
|
446
|
+
.command('init')
|
|
447
|
+
.description('Initialize an add-on from the current project')
|
|
448
|
+
.action(async () => {
|
|
449
|
+
await initAddOn(environment);
|
|
450
|
+
});
|
|
451
|
+
addOnCommand
|
|
452
|
+
.command('compile')
|
|
453
|
+
.description('Update add-on from the current project')
|
|
454
|
+
.action(async () => {
|
|
455
|
+
await compileAddOn(environment);
|
|
456
|
+
});
|
|
457
|
+
// === STARTER SUBCOMMAND ===
|
|
458
|
+
const starterCommand = program.command('starter');
|
|
459
|
+
starterCommand
|
|
460
|
+
.command('init')
|
|
461
|
+
.description('Initialize a project starter from the current project')
|
|
462
|
+
.action(async () => {
|
|
463
|
+
await initStarter(environment);
|
|
464
|
+
});
|
|
465
|
+
starterCommand
|
|
466
|
+
.command('compile')
|
|
467
|
+
.description('Compile the starter JSON file for the current project')
|
|
468
|
+
.action(async () => {
|
|
469
|
+
await compileStarter(environment);
|
|
470
|
+
});
|
|
471
|
+
// === LEGACY AUTO-CREATE MODE ===
|
|
472
|
+
// For backward compatibility with cli-aliases (create-tsrouter-app, etc.)
|
|
473
|
+
// If legacyAutoCreate is true and no subcommand is provided, treat the first
|
|
474
|
+
// argument as a project name and auto-invoke create behavior
|
|
475
|
+
if (legacyAutoCreate) {
|
|
476
|
+
// Configure the main program with create options for legacy mode
|
|
477
|
+
configureCreateCommand(program);
|
|
478
|
+
program.action(handleCreate);
|
|
479
|
+
}
|
|
480
|
+
program.parse();
|
|
481
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { DEFAULT_PACKAGE_MANAGER, finalizeAddOns, getFrameworkById, getPackageManager, loadStarter, populateAddOnOptionsDefaults, } from '@tanstack/create';
|
|
4
|
+
import { getCurrentDirectoryName, sanitizePackageName, validateProjectName, } from './utils.js';
|
|
5
|
+
export async function normalizeOptions(cliOptions, forcedMode, forcedAddOns, opts) {
|
|
6
|
+
let projectName = (cliOptions.projectName ?? '').trim();
|
|
7
|
+
let targetDir;
|
|
8
|
+
// Handle "." as project name - use current directory
|
|
9
|
+
if (projectName === '.') {
|
|
10
|
+
projectName = sanitizePackageName(getCurrentDirectoryName());
|
|
11
|
+
targetDir = resolve(process.cwd());
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
targetDir = resolve(process.cwd(), projectName);
|
|
15
|
+
}
|
|
16
|
+
if (!projectName && !opts?.disableNameCheck) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
if (projectName) {
|
|
20
|
+
const { valid, error } = validateProjectName(projectName);
|
|
21
|
+
if (!valid) {
|
|
22
|
+
console.error(error);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
let tailwind = !!cliOptions.tailwind;
|
|
27
|
+
let mode = forcedMode ||
|
|
28
|
+
(cliOptions.template === 'file-router' ? 'file-router' : 'code-router');
|
|
29
|
+
const starter = cliOptions.starter
|
|
30
|
+
? await loadStarter(cliOptions.starter)
|
|
31
|
+
: undefined;
|
|
32
|
+
// TODO: Make this declarative
|
|
33
|
+
let typescript = cliOptions.template === 'typescript' ||
|
|
34
|
+
cliOptions.template === 'file-router' ||
|
|
35
|
+
cliOptions.framework === 'solid';
|
|
36
|
+
if (starter) {
|
|
37
|
+
tailwind = starter.tailwind;
|
|
38
|
+
typescript = starter.typescript;
|
|
39
|
+
cliOptions.framework = starter.framework;
|
|
40
|
+
mode = starter.mode;
|
|
41
|
+
}
|
|
42
|
+
const framework = getFrameworkById(cliOptions.framework || 'react-cra');
|
|
43
|
+
if (forcedMode &&
|
|
44
|
+
framework.supportedModes?.[forcedMode]?.forceTypescript !== undefined) {
|
|
45
|
+
typescript = true;
|
|
46
|
+
}
|
|
47
|
+
if (cliOptions.framework === 'solid') {
|
|
48
|
+
tailwind = true;
|
|
49
|
+
}
|
|
50
|
+
async function selectAddOns() {
|
|
51
|
+
// Edge case for Windows Powershell
|
|
52
|
+
if (Array.isArray(cliOptions.addOns) && cliOptions.addOns.length === 1) {
|
|
53
|
+
const parseSeparatedArgs = cliOptions.addOns[0].split(' ');
|
|
54
|
+
if (parseSeparatedArgs.length > 1) {
|
|
55
|
+
cliOptions.addOns = parseSeparatedArgs;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(cliOptions.addOns) ||
|
|
59
|
+
starter?.dependsOn ||
|
|
60
|
+
forcedAddOns ||
|
|
61
|
+
cliOptions.toolchain ||
|
|
62
|
+
cliOptions.deployment) {
|
|
63
|
+
const selectedAddOns = new Set([
|
|
64
|
+
...(starter?.dependsOn || []),
|
|
65
|
+
...(forcedAddOns || []),
|
|
66
|
+
]);
|
|
67
|
+
if (cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
|
|
68
|
+
for (const a of cliOptions.addOns) {
|
|
69
|
+
selectedAddOns.add(a);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (cliOptions.toolchain) {
|
|
73
|
+
selectedAddOns.add(cliOptions.toolchain);
|
|
74
|
+
}
|
|
75
|
+
if (cliOptions.deployment) {
|
|
76
|
+
selectedAddOns.add(cliOptions.deployment);
|
|
77
|
+
}
|
|
78
|
+
if (!cliOptions.deployment && opts?.forcedDeployment) {
|
|
79
|
+
selectedAddOns.add(opts.forcedDeployment);
|
|
80
|
+
}
|
|
81
|
+
return await finalizeAddOns(framework, mode, Array.from(selectedAddOns));
|
|
82
|
+
}
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const chosenAddOns = await selectAddOns();
|
|
86
|
+
if (chosenAddOns.length) {
|
|
87
|
+
typescript = true;
|
|
88
|
+
// Check if any add-on explicitly requires tailwind
|
|
89
|
+
const addOnsRequireTailwind = chosenAddOns.some((addOn) => addOn.tailwind === true);
|
|
90
|
+
// Only set tailwind to true if:
|
|
91
|
+
// 1. An add-on explicitly requires it, OR
|
|
92
|
+
// 2. User explicitly set it via CLI
|
|
93
|
+
if (addOnsRequireTailwind) {
|
|
94
|
+
tailwind = true;
|
|
95
|
+
}
|
|
96
|
+
else if (cliOptions.tailwind === true) {
|
|
97
|
+
tailwind = true;
|
|
98
|
+
}
|
|
99
|
+
else if (cliOptions.tailwind === false) {
|
|
100
|
+
tailwind = false;
|
|
101
|
+
}
|
|
102
|
+
// If cliOptions.tailwind is undefined and no add-ons require it,
|
|
103
|
+
// leave tailwind as is (will be prompted in interactive mode)
|
|
104
|
+
}
|
|
105
|
+
// Handle add-on configuration option
|
|
106
|
+
let addOnOptionsFromCLI = {};
|
|
107
|
+
if (cliOptions.addOnConfig) {
|
|
108
|
+
try {
|
|
109
|
+
addOnOptionsFromCLI = JSON.parse(cliOptions.addOnConfig);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.error('Error parsing add-on config:', error);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
projectName: projectName,
|
|
118
|
+
targetDir,
|
|
119
|
+
framework,
|
|
120
|
+
mode,
|
|
121
|
+
typescript,
|
|
122
|
+
tailwind,
|
|
123
|
+
packageManager: cliOptions.packageManager ||
|
|
124
|
+
getPackageManager() ||
|
|
125
|
+
DEFAULT_PACKAGE_MANAGER,
|
|
126
|
+
git: !!cliOptions.git,
|
|
127
|
+
install: cliOptions.install,
|
|
128
|
+
chosenAddOns,
|
|
129
|
+
addOnOptions: {
|
|
130
|
+
...populateAddOnOptionsDefaults(chosenAddOns),
|
|
131
|
+
...addOnOptionsFromCLI,
|
|
132
|
+
},
|
|
133
|
+
starter: starter,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export function validateDevWatchOptions(cliOptions) {
|
|
137
|
+
if (!cliOptions.devWatch) {
|
|
138
|
+
return { valid: true };
|
|
139
|
+
}
|
|
140
|
+
// Validate watch path exists
|
|
141
|
+
const watchPath = resolve(process.cwd(), cliOptions.devWatch);
|
|
142
|
+
if (!fs.existsSync(watchPath)) {
|
|
143
|
+
return {
|
|
144
|
+
valid: false,
|
|
145
|
+
error: `Watch path does not exist: ${watchPath}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// Validate it's a directory
|
|
149
|
+
const stats = fs.statSync(watchPath);
|
|
150
|
+
if (!stats.isDirectory()) {
|
|
151
|
+
return {
|
|
152
|
+
valid: false,
|
|
153
|
+
error: `Watch path is not a directory: ${watchPath}`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// Ensure target directory is specified
|
|
157
|
+
if (!cliOptions.projectName && !cliOptions.targetDir) {
|
|
158
|
+
return {
|
|
159
|
+
valid: false,
|
|
160
|
+
error: 'Project name or target directory is required for dev watch mode',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// Check for framework structure
|
|
164
|
+
const hasAddOns = fs.existsSync(resolve(watchPath, 'add-ons'));
|
|
165
|
+
const hasAssets = fs.existsSync(resolve(watchPath, 'assets'));
|
|
166
|
+
const hasFrameworkJson = fs.existsSync(resolve(watchPath, 'framework.json'));
|
|
167
|
+
if (!hasAddOns && !hasAssets && !hasFrameworkJson) {
|
|
168
|
+
return {
|
|
169
|
+
valid: false,
|
|
170
|
+
error: `Watch path does not appear to be a valid framework directory: ${watchPath}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return { valid: true };
|
|
174
|
+
}
|