@tanstack/cli 0.61.1 → 0.62.1

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.
Files changed (46) hide show
  1. package/package.json +11 -5
  2. package/skills/CHANGELOG.md +18 -0
  3. package/skills/add-addons-existing-app/SKILL.md +113 -0
  4. package/skills/choose-ecosystem-integrations/SKILL.md +140 -0
  5. package/skills/choose-ecosystem-integrations/references/authentication-providers.md +19 -0
  6. package/skills/choose-ecosystem-integrations/references/data-layer-providers.md +20 -0
  7. package/skills/choose-ecosystem-integrations/references/deployment-targets.md +19 -0
  8. package/skills/create-app-scaffold/SKILL.md +132 -0
  9. package/skills/create-app-scaffold/references/create-flag-compatibility-matrix.md +34 -0
  10. package/skills/create-app-scaffold/references/deployment-providers.md +19 -0
  11. package/skills/create-app-scaffold/references/framework-adapters.md +17 -0
  12. package/skills/create-app-scaffold/references/toolchains.md +17 -0
  13. package/skills/maintain-custom-addons-dev-watch/SKILL.md +118 -0
  14. package/skills/query-docs-library-metadata/SKILL.md +85 -0
  15. package/skills/query-docs-library-metadata/references/discovery-command-output-schemas.md +70 -0
  16. package/CHANGELOG.md +0 -815
  17. package/playwright-report/index.html +0 -85
  18. package/playwright.config.ts +0 -21
  19. package/src/bin.ts +0 -15
  20. package/src/cli.ts +0 -1099
  21. package/src/command-line.ts +0 -612
  22. package/src/dev-watch.ts +0 -564
  23. package/src/discovery.ts +0 -209
  24. package/src/file-syncer.ts +0 -263
  25. package/src/index.ts +0 -21
  26. package/src/options.ts +0 -280
  27. package/src/types.ts +0 -27
  28. package/src/ui-environment.ts +0 -74
  29. package/src/ui-prompts.ts +0 -387
  30. package/src/utils.ts +0 -30
  31. package/test-results/.last-run.json +0 -4
  32. package/tests/command-line.test.ts +0 -703
  33. package/tests/index.test.ts +0 -9
  34. package/tests/options.test.ts +0 -281
  35. package/tests/setupVitest.ts +0 -6
  36. package/tests/ui-environment.test.ts +0 -97
  37. package/tests/ui-prompts.test.ts +0 -233
  38. package/tests-e2e/addons-smoke.spec.ts +0 -31
  39. package/tests-e2e/create-smoke.spec.ts +0 -39
  40. package/tests-e2e/helpers.ts +0 -526
  41. package/tests-e2e/matrix-opportunistic.spec.ts +0 -142
  42. package/tests-e2e/router-only-smoke.spec.ts +0 -54
  43. package/tests-e2e/solid-smoke.spec.ts +0 -26
  44. package/tests-e2e/templates-smoke.spec.ts +0 -52
  45. package/tsconfig.json +0 -17
  46. package/vitest.config.js +0 -8
@@ -1,612 +0,0 @@
1
- import { resolve } from 'node:path'
2
- import fs from 'node:fs'
3
-
4
- import {
5
- DEFAULT_PACKAGE_MANAGER,
6
- finalizeAddOns,
7
- getFrameworkById,
8
- getPackageManager,
9
- getRawRegistry,
10
- loadStarter,
11
- populateAddOnOptionsDefaults,
12
- } from '@tanstack/create'
13
-
14
- import {
15
- getCurrentDirectoryName,
16
- sanitizePackageName,
17
- validateProjectName,
18
- } from './utils.js'
19
- import type { Options } from '@tanstack/create'
20
-
21
- import type { CliOptions } from './types.js'
22
-
23
- const SUPPORTED_LEGACY_TEMPLATES = new Set([
24
- 'file-router',
25
- 'typescript',
26
- 'tsx',
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 humanizeStarterId(value: string) {
56
- return value
57
- .split(/[-_]/g)
58
- .filter(Boolean)
59
- .map((part) => part[0].toUpperCase() + part.slice(1))
60
- .join(' ')
61
- }
62
-
63
- function isLikelyStarterUrlOrPath(value: string) {
64
- return (
65
- /^https?:\/\//i.test(value) ||
66
- /^file:\/\//i.test(value) ||
67
- value.startsWith('./') ||
68
- value.startsWith('../') ||
69
- value.startsWith('/') ||
70
- /^[a-zA-Z]:[\\/]/.test(value)
71
- )
72
- }
73
-
74
- function getStarterIdsFromUrl(starterUrl: string) {
75
- const ids = new Set<string>()
76
-
77
- try {
78
- const pathname = new URL(starterUrl).pathname
79
- const parts = pathname.split('/').filter(Boolean)
80
- const lastPart = parts.at(-1)?.toLowerCase()
81
-
82
- if (lastPart) {
83
- ids.add(lastPart.replace(/\.json$/i, ''))
84
- }
85
-
86
- if (
87
- (lastPart === 'starter.json' || lastPart === 'template.json') &&
88
- parts.length >= 2
89
- ) {
90
- ids.add(parts.at(-2)!.toLowerCase())
91
- }
92
- } catch {
93
- // Ignore URL parse errors and rely on other id heuristics.
94
- }
95
-
96
- return ids
97
- }
98
-
99
- function resolveMonorepoStarterById(
100
- starterId: string,
101
- preferredFramework?: string,
102
- ) {
103
- const normalized = starterId.toLowerCase().trim()
104
- const idVariants = Array.from(
105
- new Set([
106
- normalized,
107
- normalized.replace(/-template$/i, ''),
108
- normalized.replace(/-starter$/i, ''),
109
- ]),
110
- ).filter(Boolean)
111
-
112
- const cwd = process.cwd()
113
- const rootCandidates = [
114
- cwd,
115
- resolve(cwd, '..'),
116
- resolve(cwd, '../..'),
117
- resolve(cwd, '../../..'),
118
- ]
119
-
120
- const frameworkOrder = preferredFramework
121
- ? [preferredFramework, ...['react', 'solid'].filter((f) => f !== preferredFramework)]
122
- : ['react', 'solid']
123
-
124
- for (const root of rootCandidates) {
125
- for (const framework of frameworkOrder) {
126
- for (const id of idVariants) {
127
- const templatePath = resolve(root, 'examples', framework, id, 'template.json')
128
- if (fs.existsSync(templatePath)) {
129
- return templatePath
130
- }
131
-
132
- const starterPath = resolve(root, 'examples', framework, id, 'starter.json')
133
- if (fs.existsSync(starterPath)) {
134
- return starterPath
135
- }
136
- }
137
- }
138
- }
139
-
140
- return undefined
141
- }
142
-
143
- export async function resolveStarterSpecifier(
144
- starterSpecifier: string,
145
- preferredFramework?: string,
146
- ) {
147
- const normalized = starterSpecifier.trim()
148
-
149
- if (!normalized || isLikelyStarterUrlOrPath(normalized)) {
150
- return normalized
151
- }
152
-
153
- const registry = await getRawRegistry()
154
- if (registry && registry.starters?.length) {
155
- const lookup = normalized.toLowerCase()
156
- const matches = registry.starters.filter((starter) => {
157
- const candidateIds = new Set<string>()
158
- candidateIds.add(starter.name.toLowerCase())
159
- candidateIds.add(slugifyStarterName(starter.name))
160
-
161
- for (const id of getStarterIdsFromUrl(starter.url)) {
162
- candidateIds.add(id)
163
- }
164
-
165
- return candidateIds.has(lookup)
166
- })
167
-
168
- const frameworkMatch = preferredFramework
169
- ? matches.find(
170
- (starter) => starter.framework.toLowerCase() === preferredFramework,
171
- )
172
- : undefined
173
-
174
- if (frameworkMatch) {
175
- return frameworkMatch.url
176
- }
177
-
178
- if (matches.length > 0) {
179
- return matches[0].url
180
- }
181
- }
182
-
183
- const monorepoStarterPath = resolveMonorepoStarterById(
184
- normalized,
185
- preferredFramework,
186
- )
187
- if (monorepoStarterPath) {
188
- return monorepoStarterPath
189
- }
190
-
191
- if (!registry || !registry.starters?.length) {
192
- throw new Error(
193
- `Could not resolve template id "${normalized}" because no template registry is configured. Pass a template URL (or set CTA_REGISTRY).`,
194
- )
195
- }
196
-
197
- const availableIds = Array.from(
198
- new Set(
199
- registry.starters.flatMap((starter) => {
200
- const ids = [slugifyStarterName(starter.name)]
201
- ids.push(...Array.from(getStarterIdsFromUrl(starter.url)))
202
- return ids
203
- }),
204
- ),
205
- )
206
- .filter(Boolean)
207
- .sort()
208
-
209
- throw new Error(
210
- `Unknown template id "${normalized}". Available built-in templates: ${availableIds.join(', ')}`,
211
- )
212
- }
213
-
214
- export async function listTemplateChoices(preferredFramework?: string): Promise<
215
- Array<{
216
- id: string
217
- name: string
218
- description?: string
219
- framework: string
220
- }>
221
- > {
222
- const frameworkFilter = preferredFramework?.toLowerCase()
223
- const deduped = new Map<
224
- string,
225
- { id: string; name: string; description?: string; framework: string }
226
- >()
227
-
228
- const registry = await getRawRegistry()
229
- for (const starter of registry?.starters || []) {
230
- const framework = starter.framework.toLowerCase()
231
- if (frameworkFilter && framework !== frameworkFilter) {
232
- continue
233
- }
234
-
235
- const ids = Array.from(getStarterIdsFromUrl(starter.url))
236
- const id = ids[0] || slugifyStarterName(starter.name)
237
- if (!id) {
238
- continue
239
- }
240
-
241
- const key = `${framework}:${id}`
242
- if (!deduped.has(key)) {
243
- deduped.set(key, {
244
- id,
245
- name: starter.name,
246
- description: starter.description,
247
- framework,
248
- })
249
- }
250
- }
251
-
252
- const cwd = process.cwd()
253
- const rootCandidates = [
254
- cwd,
255
- resolve(cwd, '..'),
256
- resolve(cwd, '../..'),
257
- resolve(cwd, '../../..'),
258
- ]
259
-
260
- const frameworks = frameworkFilter ? [frameworkFilter] : ['react', 'solid']
261
-
262
- for (const root of rootCandidates) {
263
- for (const framework of frameworks) {
264
- const frameworkDir = resolve(root, 'examples', framework)
265
- if (!fs.existsSync(frameworkDir) || !fs.statSync(frameworkDir).isDirectory()) {
266
- continue
267
- }
268
-
269
- for (const entry of fs.readdirSync(frameworkDir, { withFileTypes: true })) {
270
- if (!entry.isDirectory()) {
271
- continue
272
- }
273
-
274
- const id = entry.name
275
- const key = `${framework}:${id}`
276
- if (deduped.has(key)) {
277
- continue
278
- }
279
-
280
- const templatePath = resolve(frameworkDir, id, 'template.json')
281
- const starterPath = resolve(frameworkDir, id, 'starter.json')
282
- if (!fs.existsSync(templatePath) && !fs.existsSync(starterPath)) {
283
- continue
284
- }
285
-
286
- let name = humanizeStarterId(id)
287
- let description: string | undefined
288
-
289
- const templateInfoPath = resolve(frameworkDir, id, 'template-info.json')
290
- if (fs.existsSync(templateInfoPath)) {
291
- try {
292
- const info = JSON.parse(fs.readFileSync(templateInfoPath, 'utf8')) as {
293
- name?: string
294
- description?: string
295
- }
296
- if (info.name) {
297
- name = info.name
298
- }
299
- description = info.description
300
- } catch {
301
- // Ignore malformed template-info files and use fallback values.
302
- }
303
- }
304
-
305
- deduped.set(key, {
306
- id,
307
- name,
308
- description,
309
- framework,
310
- })
311
- }
312
- }
313
- }
314
-
315
- return Array.from(deduped.values()).sort((a, b) => a.name.localeCompare(b.name))
316
- }
317
-
318
- export function validateLegacyCreateFlags(cliOptions: CliOptions): {
319
- warnings: Array<string>
320
- error?: string
321
- } {
322
- const warnings: Array<string> = []
323
- const legacyTemplate = getLegacyTemplateValue(cliOptions.template)
324
-
325
- if (cliOptions.starter) {
326
- warnings.push(
327
- 'The --starter flag is deprecated; prefer --template instead. Backward compatibility remains for now.',
328
- )
329
- }
330
-
331
- if (cliOptions.starter && cliOptions.template && !legacyTemplate) {
332
- warnings.push(
333
- 'Both --starter and --template were provided. --template takes precedence.',
334
- )
335
- }
336
-
337
- if (cliOptions.routerOnly) {
338
- warnings.push(
339
- '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.',
340
- )
341
- }
342
-
343
- if (cliOptions.routerOnly && cliOptions.addOns) {
344
- warnings.push(
345
- 'Ignoring --add-ons in router-only compatibility mode.',
346
- )
347
- }
348
-
349
- if (cliOptions.routerOnly && cliOptions.deployment) {
350
- warnings.push(
351
- 'Ignoring --deployment in router-only compatibility mode.',
352
- )
353
- }
354
-
355
- if (cliOptions.routerOnly && cliOptions.starter) {
356
- warnings.push('Ignoring --starter/--template in router-only compatibility mode.')
357
- }
358
-
359
- if (cliOptions.routerOnly && cliOptions.templateId) {
360
- warnings.push('Ignoring --template-id in router-only compatibility mode.')
361
- }
362
-
363
- if (cliOptions.tailwind === true) {
364
- warnings.push(
365
- 'The --tailwind flag is deprecated and ignored. Tailwind is always enabled in TanStack Start scaffolds.',
366
- )
367
- }
368
-
369
- if (cliOptions.tailwind === false) {
370
- warnings.push(
371
- '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.',
372
- )
373
- }
374
-
375
- if (!legacyTemplate) {
376
- return { warnings }
377
- }
378
-
379
- const template = legacyTemplate
380
-
381
- if (template === 'javascript' || template === 'js' || template === 'jsx') {
382
- return {
383
- warnings,
384
- error:
385
- 'JavaScript/JSX templates are not supported. TanStack Start file-router templates are TypeScript-only.',
386
- }
387
- }
388
-
389
- if (!SUPPORTED_LEGACY_TEMPLATES.has(template)) {
390
- return {
391
- warnings,
392
- error: `Invalid --template value: ${cliOptions.template}. Supported values are: file-router, typescript, tsx.`,
393
- }
394
- }
395
-
396
- warnings.push('The --template flag is deprecated and mapped for compatibility.')
397
-
398
- return { warnings }
399
- }
400
-
401
- export async function normalizeOptions(
402
- cliOptions: CliOptions,
403
- forcedAddOns?: Array<string>,
404
- opts?: {
405
- disableNameCheck?: boolean
406
- forcedDeployment?: string
407
- },
408
- ): Promise<Options | undefined> {
409
- let projectName = (cliOptions.projectName ?? '').trim()
410
- let targetDir: string
411
-
412
- // Handle "." as project name - use current directory
413
- if (projectName === '.') {
414
- projectName = sanitizePackageName(getCurrentDirectoryName())
415
- targetDir = resolve(process.cwd())
416
- } else {
417
- targetDir = resolve(process.cwd(), projectName)
418
- }
419
-
420
- if (!projectName && !opts?.disableNameCheck) {
421
- return undefined
422
- }
423
-
424
- if (projectName) {
425
- const { valid, error } = validateProjectName(projectName)
426
- if (!valid) {
427
- console.error(error)
428
- process.exit(1)
429
- }
430
- }
431
-
432
- // Mode is always file-router (TanStack Start)
433
- let mode = 'file-router'
434
- let routerOnly = !!cliOptions.routerOnly
435
-
436
- const legacyTemplate = getLegacyTemplateValue(cliOptions.template)
437
-
438
- if (!cliOptions.starter) {
439
- if (cliOptions.template && !legacyTemplate) {
440
- cliOptions.starter = cliOptions.template
441
- } else if (cliOptions.templateId) {
442
- cliOptions.starter = cliOptions.templateId
443
- }
444
- }
445
-
446
- const template = legacyTemplate
447
- if (template && template !== 'file-router') {
448
- routerOnly = true
449
- }
450
-
451
- if (!cliOptions.starter && cliOptions.templateId) {
452
- cliOptions.starter = cliOptions.templateId
453
- }
454
-
455
- const preferredFramework = (cliOptions.framework || 'react').toLowerCase()
456
-
457
- const starter = !routerOnly && cliOptions.starter
458
- ? await loadStarter(
459
- await resolveStarterSpecifier(cliOptions.starter, preferredFramework),
460
- )
461
- : undefined
462
-
463
- // TypeScript and Tailwind are always enabled with TanStack Start
464
- const typescript = true
465
- const tailwind = true
466
-
467
- if (starter) {
468
- cliOptions.framework = starter.framework
469
- mode = starter.mode
470
- }
471
-
472
- const framework = getFrameworkById(cliOptions.framework || 'react')!
473
-
474
- async function selectAddOns() {
475
- // Edge case for Windows Powershell
476
- if (Array.isArray(cliOptions.addOns) && cliOptions.addOns.length === 1) {
477
- const parseSeparatedArgs = cliOptions.addOns[0].split(' ')
478
- if (parseSeparatedArgs.length > 1) {
479
- cliOptions.addOns = parseSeparatedArgs
480
- }
481
- }
482
-
483
- if (
484
- Array.isArray(cliOptions.addOns) ||
485
- starter?.dependsOn ||
486
- forcedAddOns ||
487
- cliOptions.toolchain ||
488
- cliOptions.deployment
489
- ) {
490
- const selectedAddOns = new Set<string>([
491
- ...(routerOnly ? [] : (starter?.dependsOn || [])),
492
- ...(routerOnly ? [] : (forcedAddOns || [])),
493
- ])
494
- if (!routerOnly && cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
495
- for (const a of cliOptions.addOns) {
496
- if (a.toLowerCase() === 'start') {
497
- continue
498
- }
499
- selectedAddOns.add(a)
500
- }
501
- }
502
- if (cliOptions.toolchain) {
503
- selectedAddOns.add(cliOptions.toolchain)
504
- }
505
- if (!routerOnly && cliOptions.deployment) {
506
- selectedAddOns.add(cliOptions.deployment)
507
- }
508
-
509
- if (!routerOnly && !cliOptions.deployment && opts?.forcedDeployment) {
510
- selectedAddOns.add(opts.forcedDeployment)
511
- }
512
-
513
- return await finalizeAddOns(framework, mode, Array.from(selectedAddOns))
514
- }
515
-
516
- return []
517
- }
518
-
519
- const includeExamples = cliOptions.examples ?? !routerOnly
520
- const chosenAddOnsRaw = await selectAddOns()
521
- const chosenAddOns = includeExamples
522
- ? chosenAddOnsRaw
523
- : chosenAddOnsRaw.filter((addOn) => addOn.type !== 'example')
524
-
525
- // Handle add-on configuration option
526
- let addOnOptionsFromCLI = {}
527
- if (cliOptions.addOnConfig) {
528
- try {
529
- addOnOptionsFromCLI = JSON.parse(cliOptions.addOnConfig)
530
- } catch (error) {
531
- console.error('Error parsing add-on config:', error)
532
- process.exit(1)
533
- }
534
- }
535
-
536
- const normalized = {
537
- projectName: projectName,
538
- targetDir,
539
- framework,
540
- mode,
541
- typescript,
542
- tailwind,
543
- packageManager:
544
- cliOptions.packageManager ||
545
- getPackageManager() ||
546
- DEFAULT_PACKAGE_MANAGER,
547
- git: cliOptions.git ?? true,
548
- install: cliOptions.install,
549
- chosenAddOns,
550
- addOnOptions: {
551
- ...populateAddOnOptionsDefaults(chosenAddOns),
552
- ...addOnOptionsFromCLI,
553
- },
554
- starter: starter,
555
- }
556
-
557
- ;(normalized as Options & { includeExamples?: boolean }).includeExamples =
558
- includeExamples
559
- ;(normalized as Options & { envVarValues?: Record<string, string> }).envVarValues =
560
- {}
561
-
562
- return normalized
563
- }
564
-
565
- export function validateDevWatchOptions(cliOptions: CliOptions): {
566
- valid: boolean
567
- error?: string
568
- } {
569
- if (!cliOptions.devWatch) {
570
- return { valid: true }
571
- }
572
-
573
- // Validate watch path exists
574
- const watchPath = resolve(process.cwd(), cliOptions.devWatch)
575
- if (!fs.existsSync(watchPath)) {
576
- return {
577
- valid: false,
578
- error: `Watch path does not exist: ${watchPath}`,
579
- }
580
- }
581
-
582
- // Validate it's a directory
583
- const stats = fs.statSync(watchPath)
584
- if (!stats.isDirectory()) {
585
- return {
586
- valid: false,
587
- error: `Watch path is not a directory: ${watchPath}`,
588
- }
589
- }
590
-
591
- // Ensure target directory is specified
592
- if (!cliOptions.projectName && !cliOptions.targetDir) {
593
- return {
594
- valid: false,
595
- error: 'Project name or target directory is required for dev watch mode',
596
- }
597
- }
598
-
599
- // Check for framework structure
600
- const hasAddOns = fs.existsSync(resolve(watchPath, 'add-ons'))
601
- const hasAssets = fs.existsSync(resolve(watchPath, 'assets'))
602
- const hasFrameworkJson = fs.existsSync(resolve(watchPath, 'framework.json'))
603
-
604
- if (!hasAddOns && !hasAssets && !hasFrameworkJson) {
605
- return {
606
- valid: false,
607
- error: `Watch path does not appear to be a valid framework directory: ${watchPath}`,
608
- }
609
- }
610
-
611
- return { valid: true }
612
- }