@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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
testDir: './tests-e2e',
|
|
5
|
+
fullyParallel: false,
|
|
6
|
+
forbidOnly: !!process.env.CI,
|
|
7
|
+
retries: process.env.CI ? 1 : 0,
|
|
8
|
+
workers: 1,
|
|
9
|
+
timeout: 10 * 60 * 1000,
|
|
10
|
+
expect: {
|
|
11
|
+
timeout: 20 * 1000,
|
|
12
|
+
},
|
|
13
|
+
reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : 'list',
|
|
14
|
+
use: {
|
|
15
|
+
...devices['Desktop Chrome'],
|
|
16
|
+
channel: 'chrome',
|
|
17
|
+
trace: 'on-first-retry',
|
|
18
|
+
screenshot: 'only-on-failure',
|
|
19
|
+
video: 'retain-on-failure',
|
|
20
|
+
},
|
|
21
|
+
})
|
package/src/bin.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { cli } from './cli.js'
|
|
3
|
+
import {
|
|
4
|
+
createReactFrameworkDefinition,
|
|
5
|
+
createSolidFrameworkDefinition,
|
|
6
|
+
} from '@tanstack/create'
|
|
3
7
|
|
|
4
8
|
cli({
|
|
5
9
|
name: 'tanstack',
|
|
6
10
|
appName: 'TanStack',
|
|
11
|
+
frameworkDefinitionInitializers: [
|
|
12
|
+
createReactFrameworkDefinition,
|
|
13
|
+
createSolidFrameworkDefinition,
|
|
14
|
+
],
|
|
7
15
|
})
|
package/src/cli.ts
CHANGED
|
@@ -9,10 +9,9 @@ import {
|
|
|
9
9
|
SUPPORTED_PACKAGE_MANAGERS,
|
|
10
10
|
addToApp,
|
|
11
11
|
compileAddOn,
|
|
12
|
-
devAddOn,
|
|
13
12
|
compileStarter,
|
|
14
13
|
createApp,
|
|
15
|
-
|
|
14
|
+
devAddOn,
|
|
16
15
|
getAllAddOns,
|
|
17
16
|
getFrameworkByName,
|
|
18
17
|
getFrameworks,
|
|
@@ -20,8 +19,6 @@ import {
|
|
|
20
19
|
initStarter,
|
|
21
20
|
} from '@tanstack/create'
|
|
22
21
|
|
|
23
|
-
import { launchUI } from '@tanstack/create-ui'
|
|
24
|
-
|
|
25
22
|
import { runMCPServer } from './mcp.js'
|
|
26
23
|
|
|
27
24
|
import { promptForAddOns, promptForCreateOptions } from './options.js'
|
|
@@ -52,7 +49,6 @@ export function cli({
|
|
|
52
49
|
forcedAddOns = [],
|
|
53
50
|
forcedDeployment,
|
|
54
51
|
defaultFramework,
|
|
55
|
-
webBase,
|
|
56
52
|
frameworkDefinitionInitializers,
|
|
57
53
|
showDeploymentOptions = false,
|
|
58
54
|
legacyAutoCreate = false,
|
|
@@ -63,7 +59,6 @@ export function cli({
|
|
|
63
59
|
forcedAddOns?: Array<string>
|
|
64
60
|
forcedDeployment?: string
|
|
65
61
|
defaultFramework?: string
|
|
66
|
-
webBase?: string
|
|
67
62
|
frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>
|
|
68
63
|
showDeploymentOptions?: boolean
|
|
69
64
|
legacyAutoCreate?: boolean
|
|
@@ -106,6 +101,91 @@ export function cli({
|
|
|
106
101
|
|
|
107
102
|
const availableFrameworks = getFrameworks().map((f) => f.name)
|
|
108
103
|
|
|
104
|
+
function resolveBuiltInDevWatchPath(frameworkId: string): string {
|
|
105
|
+
const candidates = [
|
|
106
|
+
resolve(process.cwd(), 'packages/create/src/frameworks', frameworkId),
|
|
107
|
+
resolve(process.cwd(), '../create/src/frameworks', frameworkId),
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
for (const candidate of candidates) {
|
|
111
|
+
if (fs.existsSync(candidate)) {
|
|
112
|
+
return candidate
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return candidates[0]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function startDevWatchMode(projectName: string, options: CliOptions) {
|
|
120
|
+
// Validate dev watch options
|
|
121
|
+
const validation = validateDevWatchOptions({ ...options, projectName })
|
|
122
|
+
if (!validation.valid) {
|
|
123
|
+
console.error(validation.error)
|
|
124
|
+
process.exit(1)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Enter dev watch mode
|
|
128
|
+
if (!projectName && !options.targetDir) {
|
|
129
|
+
console.error('Project name/target directory is required for dev watch mode')
|
|
130
|
+
process.exit(1)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!options.framework) {
|
|
134
|
+
console.error('Failed to detect framework')
|
|
135
|
+
process.exit(1)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const framework = getFrameworkByName(options.framework)
|
|
139
|
+
if (!framework) {
|
|
140
|
+
console.error('Failed to detect framework')
|
|
141
|
+
process.exit(1)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// First, create the app normally using the standard flow
|
|
145
|
+
const normalizedOpts = await normalizeOptions(
|
|
146
|
+
{
|
|
147
|
+
...options,
|
|
148
|
+
projectName,
|
|
149
|
+
framework: framework.id,
|
|
150
|
+
},
|
|
151
|
+
forcedAddOns,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if (!normalizedOpts) {
|
|
155
|
+
throw new Error('Failed to normalize options')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
normalizedOpts.targetDir =
|
|
159
|
+
options.targetDir || resolve(process.cwd(), projectName)
|
|
160
|
+
|
|
161
|
+
// Create the initial app with minimal output for dev watch mode
|
|
162
|
+
console.log(chalk.bold('\ndev-watch'))
|
|
163
|
+
console.log(chalk.gray('├─') + ' ' + `creating initial ${appName} app...`)
|
|
164
|
+
if (normalizedOpts.install !== false) {
|
|
165
|
+
console.log(
|
|
166
|
+
chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...',
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
const silentEnvironment = createUIEnvironment(appName, true)
|
|
170
|
+
await confirmTargetDirectorySafety(normalizedOpts.targetDir, options.force)
|
|
171
|
+
await createApp(silentEnvironment, normalizedOpts)
|
|
172
|
+
console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`)
|
|
173
|
+
|
|
174
|
+
// Now start the dev watch mode
|
|
175
|
+
const manager = new DevWatchManager({
|
|
176
|
+
watchPath: options.devWatch!,
|
|
177
|
+
targetDir: normalizedOpts.targetDir,
|
|
178
|
+
framework,
|
|
179
|
+
cliOptions: normalizedOpts,
|
|
180
|
+
packageManager: normalizedOpts.packageManager,
|
|
181
|
+
runDevCommand: options.runDev,
|
|
182
|
+
environment,
|
|
183
|
+
frameworkDefinitionInitializers,
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
await manager.start()
|
|
187
|
+
}
|
|
188
|
+
|
|
109
189
|
const toolchains = new Set<string>()
|
|
110
190
|
for (const framework of getFrameworks()) {
|
|
111
191
|
for (const addOn of framework.getAddOns()) {
|
|
@@ -235,72 +315,7 @@ export function cli({
|
|
|
235
315
|
}
|
|
236
316
|
|
|
237
317
|
if (options.devWatch) {
|
|
238
|
-
|
|
239
|
-
const validation = validateDevWatchOptions({ ...options, projectName })
|
|
240
|
-
if (!validation.valid) {
|
|
241
|
-
console.error(validation.error)
|
|
242
|
-
process.exit(1)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Enter dev watch mode
|
|
246
|
-
if (!projectName && !options.targetDir) {
|
|
247
|
-
console.error(
|
|
248
|
-
'Project name/target directory is required for dev watch mode',
|
|
249
|
-
)
|
|
250
|
-
process.exit(1)
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (!options.framework) {
|
|
254
|
-
console.error('Failed to detect framework')
|
|
255
|
-
process.exit(1)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const framework = getFrameworkByName(options.framework)
|
|
259
|
-
if (!framework) {
|
|
260
|
-
console.error('Failed to detect framework')
|
|
261
|
-
process.exit(1)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// First, create the app normally using the standard flow
|
|
265
|
-
const normalizedOpts = await normalizeOptions(
|
|
266
|
-
{
|
|
267
|
-
...options,
|
|
268
|
-
projectName,
|
|
269
|
-
framework: framework.id,
|
|
270
|
-
},
|
|
271
|
-
forcedAddOns,
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
if (!normalizedOpts) {
|
|
275
|
-
throw new Error('Failed to normalize options')
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
normalizedOpts.targetDir =
|
|
279
|
-
options.targetDir || resolve(process.cwd(), projectName)
|
|
280
|
-
|
|
281
|
-
// Create the initial app with minimal output for dev watch mode
|
|
282
|
-
console.log(chalk.bold('\ndev-watch'))
|
|
283
|
-
console.log(chalk.gray('├─') + ' ' + `creating initial ${appName} app...`)
|
|
284
|
-
if (normalizedOpts.install !== false) {
|
|
285
|
-
console.log(chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...')
|
|
286
|
-
}
|
|
287
|
-
const silentEnvironment = createUIEnvironment(appName, true)
|
|
288
|
-
await confirmTargetDirectorySafety(normalizedOpts.targetDir, options.force)
|
|
289
|
-
await createApp(silentEnvironment, normalizedOpts)
|
|
290
|
-
console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`)
|
|
291
|
-
|
|
292
|
-
// Now start the dev watch mode
|
|
293
|
-
const manager = new DevWatchManager({
|
|
294
|
-
watchPath: options.devWatch,
|
|
295
|
-
targetDir: normalizedOpts.targetDir,
|
|
296
|
-
framework,
|
|
297
|
-
cliOptions: normalizedOpts,
|
|
298
|
-
packageManager: normalizedOpts.packageManager,
|
|
299
|
-
environment,
|
|
300
|
-
frameworkDefinitionInitializers,
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
await manager.start()
|
|
318
|
+
await startDevWatchMode(projectName, options)
|
|
304
319
|
return
|
|
305
320
|
}
|
|
306
321
|
|
|
@@ -317,6 +332,9 @@ export function cli({
|
|
|
317
332
|
if (
|
|
318
333
|
cliOptions.routerOnly !== true &&
|
|
319
334
|
cliOptions.template &&
|
|
335
|
+
['file-router', 'typescript', 'tsx', 'javascript', 'js', 'jsx'].includes(
|
|
336
|
+
cliOptions.template.toLowerCase(),
|
|
337
|
+
) &&
|
|
320
338
|
cliOptions.template.toLowerCase() !== 'file-router'
|
|
321
339
|
) {
|
|
322
340
|
cliOptions.routerOnly = true
|
|
@@ -337,29 +355,6 @@ export function cli({
|
|
|
337
355
|
)
|
|
338
356
|
}
|
|
339
357
|
|
|
340
|
-
if (options.ui) {
|
|
341
|
-
const optionsFromCLI = await normalizeOptions(
|
|
342
|
-
cliOptions,
|
|
343
|
-
forcedAddOns,
|
|
344
|
-
{ disableNameCheck: true, forcedDeployment },
|
|
345
|
-
)
|
|
346
|
-
const uiOptions = {
|
|
347
|
-
...createSerializedOptions(optionsFromCLI!),
|
|
348
|
-
projectName: 'my-app',
|
|
349
|
-
targetDir: resolve(process.cwd(), 'my-app'),
|
|
350
|
-
}
|
|
351
|
-
launchUI({
|
|
352
|
-
mode: 'setup',
|
|
353
|
-
options: uiOptions,
|
|
354
|
-
forcedRouterMode: defaultMode,
|
|
355
|
-
forcedAddOns,
|
|
356
|
-
environmentFactory: () => createUIEnvironment(appName, false),
|
|
357
|
-
webBase,
|
|
358
|
-
showDeploymentOptions,
|
|
359
|
-
})
|
|
360
|
-
return
|
|
361
|
-
}
|
|
362
|
-
|
|
363
358
|
if (finalOptions) {
|
|
364
359
|
intro(`Creating a new ${appName} app in ${projectName}...`)
|
|
365
360
|
} else {
|
|
@@ -411,6 +406,10 @@ export function cli({
|
|
|
411
406
|
'--framework <type>',
|
|
412
407
|
`project framework (${availableFrameworks.join(', ')})`,
|
|
413
408
|
(value) => {
|
|
409
|
+
if (value.toLowerCase() === 'react-cra') {
|
|
410
|
+
return 'react'
|
|
411
|
+
}
|
|
412
|
+
|
|
414
413
|
if (
|
|
415
414
|
!availableFrameworks.some(
|
|
416
415
|
(f) => f.toLowerCase() === value.toLowerCase(),
|
|
@@ -428,10 +427,15 @@ export function cli({
|
|
|
428
427
|
|
|
429
428
|
cmd
|
|
430
429
|
.option(
|
|
431
|
-
'--starter [url]',
|
|
432
|
-
'
|
|
430
|
+
'--starter [url-or-id]',
|
|
431
|
+
'DEPRECATED: use --template. Initializes from a template URL or built-in id',
|
|
433
432
|
false,
|
|
434
433
|
)
|
|
434
|
+
.option('--template-id <id>', 'initialize using a built-in template id')
|
|
435
|
+
.option(
|
|
436
|
+
'--template [url-or-id]',
|
|
437
|
+
'initialize this project from a template URL or built-in template id',
|
|
438
|
+
)
|
|
435
439
|
.option('--no-install', 'skip installing dependencies')
|
|
436
440
|
.option<PackageManager>(
|
|
437
441
|
`--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`,
|
|
@@ -451,14 +455,11 @@ export function cli({
|
|
|
451
455
|
'--dev-watch <path>',
|
|
452
456
|
'Watch a framework directory for changes and auto-rebuild',
|
|
453
457
|
)
|
|
458
|
+
.option('--run-dev', 'Run the app dev server alongside dev-watch', false)
|
|
454
459
|
.option(
|
|
455
460
|
'--router-only',
|
|
456
461
|
'Use router-only compatibility mode (file-based routing without TanStack Start)',
|
|
457
462
|
)
|
|
458
|
-
.option(
|
|
459
|
-
'--template <type>',
|
|
460
|
-
'Deprecated: compatibility flag from create-tsrouter-app',
|
|
461
|
-
)
|
|
462
463
|
.option(
|
|
463
464
|
'--tailwind',
|
|
464
465
|
'Deprecated: compatibility flag; Tailwind is always enabled',
|
|
@@ -530,7 +531,6 @@ export function cli({
|
|
|
530
531
|
'--target-dir <path>',
|
|
531
532
|
'the target directory for the application root',
|
|
532
533
|
)
|
|
533
|
-
.option('--ui', 'Launch the UI for project creation')
|
|
534
534
|
.option(
|
|
535
535
|
'--add-on-config <config>',
|
|
536
536
|
'JSON string with add-on configuration options',
|
|
@@ -553,6 +553,34 @@ export function cli({
|
|
|
553
553
|
configureCreateCommand(createCommand)
|
|
554
554
|
createCommand.action(handleCreate)
|
|
555
555
|
|
|
556
|
+
// === DEV SUBCOMMAND ===
|
|
557
|
+
const devCommand = program
|
|
558
|
+
.command('dev')
|
|
559
|
+
.description(
|
|
560
|
+
'Create a sandbox app and watch built-in framework templates/add-ons',
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
configureCreateCommand(devCommand)
|
|
564
|
+
devCommand.action(async (projectName: string, options: CliOptions) => {
|
|
565
|
+
const frameworkName = options.framework || defaultFramework || 'React'
|
|
566
|
+
const framework = getFrameworkByName(frameworkName)
|
|
567
|
+
if (!framework) {
|
|
568
|
+
console.error(`Unknown framework: ${frameworkName}`)
|
|
569
|
+
process.exit(1)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const watchPath = resolveBuiltInDevWatchPath(framework.id)
|
|
573
|
+
const devOptions: CliOptions = {
|
|
574
|
+
...options,
|
|
575
|
+
framework: framework.name,
|
|
576
|
+
devWatch: watchPath,
|
|
577
|
+
runDev: true,
|
|
578
|
+
install: options.install ?? true,
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
await startDevWatchMode(projectName, devOptions)
|
|
582
|
+
})
|
|
583
|
+
|
|
556
584
|
// === MCP SUBCOMMAND ===
|
|
557
585
|
program
|
|
558
586
|
.command('mcp')
|
|
@@ -647,8 +675,7 @@ Remove your node_modules directory and package lock file and re-install.`,
|
|
|
647
675
|
'Name of the add-ons (or add-ons separated by spaces or commas)',
|
|
648
676
|
)
|
|
649
677
|
.option('--forced', 'Force the add-on to be added', false)
|
|
650
|
-
.
|
|
651
|
-
.action(async (addOns: Array<string>, options: { forced: boolean; ui: boolean }) => {
|
|
678
|
+
.action(async (addOns: Array<string>, options: { forced: boolean }) => {
|
|
652
679
|
const parsedAddOns: Array<string> = []
|
|
653
680
|
for (const addOn of addOns) {
|
|
654
681
|
if (addOn.includes(',') || addOn.includes(' ')) {
|
|
@@ -659,18 +686,7 @@ Remove your node_modules directory and package lock file and re-install.`,
|
|
|
659
686
|
parsedAddOns.push(addOn.trim())
|
|
660
687
|
}
|
|
661
688
|
}
|
|
662
|
-
if (
|
|
663
|
-
launchUI({
|
|
664
|
-
mode: 'add',
|
|
665
|
-
addOns: parsedAddOns,
|
|
666
|
-
projectPath: resolve(process.cwd()),
|
|
667
|
-
forcedRouterMode: defaultMode,
|
|
668
|
-
forcedAddOns,
|
|
669
|
-
environmentFactory: () => createUIEnvironment(appName, false),
|
|
670
|
-
webBase,
|
|
671
|
-
showDeploymentOptions,
|
|
672
|
-
})
|
|
673
|
-
} else if (parsedAddOns.length < 1) {
|
|
689
|
+
if (parsedAddOns.length < 1) {
|
|
674
690
|
const selectedAddOns = await promptForAddOns()
|
|
675
691
|
if (selectedAddOns.length) {
|
|
676
692
|
await addToApp(environment, selectedAddOns, resolve(process.cwd()), {
|
|
@@ -707,17 +723,32 @@ Remove your node_modules directory and package lock file and re-install.`,
|
|
|
707
723
|
await devAddOn(environment)
|
|
708
724
|
})
|
|
709
725
|
|
|
710
|
-
// ===
|
|
726
|
+
// === TEMPLATE SUBCOMMAND ===
|
|
727
|
+
const templateCommand = program.command('template')
|
|
728
|
+
templateCommand
|
|
729
|
+
.command('init')
|
|
730
|
+
.description('Initialize a project template from the current project')
|
|
731
|
+
.action(async () => {
|
|
732
|
+
await initStarter(environment)
|
|
733
|
+
})
|
|
734
|
+
templateCommand
|
|
735
|
+
.command('compile')
|
|
736
|
+
.description('Compile the template JSON file for the current project')
|
|
737
|
+
.action(async () => {
|
|
738
|
+
await compileStarter(environment)
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
// Legacy alias for template command
|
|
711
742
|
const starterCommand = program.command('starter')
|
|
712
743
|
starterCommand
|
|
713
744
|
.command('init')
|
|
714
|
-
.description('
|
|
745
|
+
.description('Deprecated alias: initialize a project template')
|
|
715
746
|
.action(async () => {
|
|
716
747
|
await initStarter(environment)
|
|
717
748
|
})
|
|
718
749
|
starterCommand
|
|
719
750
|
.command('compile')
|
|
720
|
-
.description('
|
|
751
|
+
.description('Deprecated alias: compile the template JSON file')
|
|
721
752
|
.action(async () => {
|
|
722
753
|
await compileStarter(environment)
|
|
723
754
|
})
|
package/src/command-line.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
finalizeAddOns,
|
|
7
7
|
getFrameworkById,
|
|
8
8
|
getPackageManager,
|
|
9
|
+
getRawRegistry,
|
|
9
10
|
loadStarter,
|
|
10
11
|
populateAddOnOptionsDefaults,
|
|
11
12
|
} from '@tanstack/create'
|
|
@@ -25,15 +26,182 @@ const SUPPORTED_LEGACY_TEMPLATES = new Set([
|
|
|
25
26
|
'tsx',
|
|
26
27
|
])
|
|
27
28
|
|
|
29
|
+
const LEGACY_TEMPLATE_ALIASES = new Set(['javascript', 'js', 'jsx'])
|
|
30
|
+
|
|
31
|
+
function getLegacyTemplateValue(templateValue?: string) {
|
|
32
|
+
if (!templateValue) {
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const normalized = templateValue.toLowerCase().trim()
|
|
37
|
+
if (
|
|
38
|
+
SUPPORTED_LEGACY_TEMPLATES.has(normalized) ||
|
|
39
|
+
LEGACY_TEMPLATE_ALIASES.has(normalized)
|
|
40
|
+
) {
|
|
41
|
+
return normalized
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function slugifyStarterName(value: string) {
|
|
48
|
+
return value
|
|
49
|
+
.trim()
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
52
|
+
.replace(/^-+|-+$/g, '')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isLikelyStarterUrlOrPath(value: string) {
|
|
56
|
+
return (
|
|
57
|
+
/^https?:\/\//i.test(value) ||
|
|
58
|
+
/^file:\/\//i.test(value) ||
|
|
59
|
+
value.startsWith('./') ||
|
|
60
|
+
value.startsWith('../') ||
|
|
61
|
+
value.startsWith('/') ||
|
|
62
|
+
/^[a-zA-Z]:[\\/]/.test(value)
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getStarterIdsFromUrl(starterUrl: string) {
|
|
67
|
+
const ids = new Set<string>()
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const pathname = new URL(starterUrl).pathname
|
|
71
|
+
const parts = pathname.split('/').filter(Boolean)
|
|
72
|
+
const lastPart = parts.at(-1)?.toLowerCase()
|
|
73
|
+
|
|
74
|
+
if (lastPart) {
|
|
75
|
+
ids.add(lastPart.replace(/\.json$/i, ''))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
(lastPart === 'starter.json' || lastPart === 'template.json') &&
|
|
80
|
+
parts.length >= 2
|
|
81
|
+
) {
|
|
82
|
+
ids.add(parts.at(-2)!.toLowerCase())
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// Ignore URL parse errors and rely on other id heuristics.
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return ids
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveMonorepoStarterById(starterId: string) {
|
|
92
|
+
const normalized = starterId.toLowerCase().trim()
|
|
93
|
+
const idVariants = Array.from(
|
|
94
|
+
new Set([
|
|
95
|
+
normalized,
|
|
96
|
+
normalized.replace(/-template$/i, ''),
|
|
97
|
+
normalized.replace(/-starter$/i, ''),
|
|
98
|
+
]),
|
|
99
|
+
).filter(Boolean)
|
|
100
|
+
|
|
101
|
+
const cwd = process.cwd()
|
|
102
|
+
const rootCandidates = [
|
|
103
|
+
cwd,
|
|
104
|
+
resolve(cwd, '..'),
|
|
105
|
+
resolve(cwd, '../..'),
|
|
106
|
+
resolve(cwd, '../../..'),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
for (const root of rootCandidates) {
|
|
110
|
+
for (const framework of ['react', 'solid']) {
|
|
111
|
+
for (const id of idVariants) {
|
|
112
|
+
const templatePath = resolve(root, 'examples', framework, id, 'template.json')
|
|
113
|
+
if (fs.existsSync(templatePath)) {
|
|
114
|
+
return templatePath
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const starterPath = resolve(root, 'examples', framework, id, 'starter.json')
|
|
118
|
+
if (fs.existsSync(starterPath)) {
|
|
119
|
+
return starterPath
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return undefined
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function resolveStarterSpecifier(starterSpecifier: string) {
|
|
129
|
+
const normalized = starterSpecifier.trim()
|
|
130
|
+
|
|
131
|
+
if (!normalized || isLikelyStarterUrlOrPath(normalized)) {
|
|
132
|
+
return normalized
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const registry = await getRawRegistry()
|
|
136
|
+
if (registry && registry.starters?.length) {
|
|
137
|
+
const lookup = normalized.toLowerCase()
|
|
138
|
+
const match = registry.starters.find((starter) => {
|
|
139
|
+
const candidateIds = new Set<string>()
|
|
140
|
+
candidateIds.add(starter.name.toLowerCase())
|
|
141
|
+
candidateIds.add(slugifyStarterName(starter.name))
|
|
142
|
+
|
|
143
|
+
for (const id of getStarterIdsFromUrl(starter.url)) {
|
|
144
|
+
candidateIds.add(id)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return candidateIds.has(lookup)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
if (match) {
|
|
151
|
+
return match.url
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const monorepoStarterPath = resolveMonorepoStarterById(normalized)
|
|
156
|
+
if (monorepoStarterPath) {
|
|
157
|
+
return monorepoStarterPath
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!registry || !registry.starters?.length) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Could not resolve template id "${normalized}" because no template registry is configured. Pass a template URL (or set CTA_REGISTRY).`,
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const availableIds = Array.from(
|
|
167
|
+
new Set(
|
|
168
|
+
registry.starters.flatMap((starter) => {
|
|
169
|
+
const ids = [slugifyStarterName(starter.name)]
|
|
170
|
+
ids.push(...Array.from(getStarterIdsFromUrl(starter.url)))
|
|
171
|
+
return ids
|
|
172
|
+
}),
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
.filter(Boolean)
|
|
176
|
+
.sort()
|
|
177
|
+
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Unknown template id "${normalized}". Available built-in templates: ${availableIds.join(', ')}`,
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
28
183
|
export function validateLegacyCreateFlags(cliOptions: CliOptions): {
|
|
29
184
|
warnings: Array<string>
|
|
30
185
|
error?: string
|
|
31
186
|
} {
|
|
32
187
|
const warnings: Array<string> = []
|
|
188
|
+
const legacyTemplate = getLegacyTemplateValue(cliOptions.template)
|
|
189
|
+
|
|
190
|
+
if (cliOptions.starter) {
|
|
191
|
+
warnings.push(
|
|
192
|
+
'The --starter flag is deprecated; prefer --template instead. Backward compatibility remains for now.',
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (cliOptions.starter && cliOptions.template && !legacyTemplate) {
|
|
197
|
+
warnings.push(
|
|
198
|
+
'Both --starter and --template were provided. --template takes precedence.',
|
|
199
|
+
)
|
|
200
|
+
}
|
|
33
201
|
|
|
34
202
|
if (cliOptions.routerOnly) {
|
|
35
203
|
warnings.push(
|
|
36
|
-
'The --router-only flag enables router-only compatibility mode. Start-dependent add-ons, deployment adapters, and
|
|
204
|
+
'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.',
|
|
37
205
|
)
|
|
38
206
|
}
|
|
39
207
|
|
|
@@ -50,7 +218,11 @@ export function validateLegacyCreateFlags(cliOptions: CliOptions): {
|
|
|
50
218
|
}
|
|
51
219
|
|
|
52
220
|
if (cliOptions.routerOnly && cliOptions.starter) {
|
|
53
|
-
warnings.push('Ignoring --starter in router-only compatibility mode.')
|
|
221
|
+
warnings.push('Ignoring --starter/--template in router-only compatibility mode.')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (cliOptions.routerOnly && cliOptions.templateId) {
|
|
225
|
+
warnings.push('Ignoring --template-id in router-only compatibility mode.')
|
|
54
226
|
}
|
|
55
227
|
|
|
56
228
|
if (cliOptions.tailwind === true) {
|
|
@@ -65,11 +237,11 @@ export function validateLegacyCreateFlags(cliOptions: CliOptions): {
|
|
|
65
237
|
)
|
|
66
238
|
}
|
|
67
239
|
|
|
68
|
-
if (!
|
|
240
|
+
if (!legacyTemplate) {
|
|
69
241
|
return { warnings }
|
|
70
242
|
}
|
|
71
243
|
|
|
72
|
-
const template =
|
|
244
|
+
const template = legacyTemplate
|
|
73
245
|
|
|
74
246
|
if (template === 'javascript' || template === 'js' || template === 'jsx') {
|
|
75
247
|
return {
|
|
@@ -126,13 +298,27 @@ export async function normalizeOptions(
|
|
|
126
298
|
let mode = 'file-router'
|
|
127
299
|
let routerOnly = !!cliOptions.routerOnly
|
|
128
300
|
|
|
129
|
-
const
|
|
301
|
+
const legacyTemplate = getLegacyTemplateValue(cliOptions.template)
|
|
302
|
+
|
|
303
|
+
if (!cliOptions.starter) {
|
|
304
|
+
if (cliOptions.template && !legacyTemplate) {
|
|
305
|
+
cliOptions.starter = cliOptions.template
|
|
306
|
+
} else if (cliOptions.templateId) {
|
|
307
|
+
cliOptions.starter = cliOptions.templateId
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const template = legacyTemplate
|
|
130
312
|
if (template && template !== 'file-router') {
|
|
131
313
|
routerOnly = true
|
|
132
314
|
}
|
|
133
315
|
|
|
316
|
+
if (!cliOptions.starter && cliOptions.templateId) {
|
|
317
|
+
cliOptions.starter = cliOptions.templateId
|
|
318
|
+
}
|
|
319
|
+
|
|
134
320
|
const starter = !routerOnly && cliOptions.starter
|
|
135
|
-
? await loadStarter(cliOptions.starter)
|
|
321
|
+
? await loadStarter(await resolveStarterSpecifier(cliOptions.starter))
|
|
136
322
|
: undefined
|
|
137
323
|
|
|
138
324
|
// TypeScript and Tailwind are always enabled with TanStack Start
|
|
@@ -144,7 +330,7 @@ export async function normalizeOptions(
|
|
|
144
330
|
mode = starter.mode
|
|
145
331
|
}
|
|
146
332
|
|
|
147
|
-
const framework = getFrameworkById(cliOptions.framework || 'react
|
|
333
|
+
const framework = getFrameworkById(cliOptions.framework || 'react')!
|
|
148
334
|
|
|
149
335
|
async function selectAddOns() {
|
|
150
336
|
// Edge case for Windows Powershell
|