@tanstack/cli 0.60.1 → 0.62.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.
Files changed (66) hide show
  1. package/dist/cli.js +266 -11
  2. package/dist/command-line.js +103 -8
  3. package/dist/discovery.js +144 -0
  4. package/dist/options.js +35 -2
  5. package/dist/types/command-line.d.ts +7 -0
  6. package/dist/types/{mcp/types.d.ts → discovery.d.ts} +23 -75
  7. package/dist/types/types.d.ts +1 -2
  8. package/dist/types/ui-prompts.d.ts +5 -0
  9. package/dist/ui-prompts.js +26 -0
  10. package/package.json +6 -5
  11. package/skills/CHANGELOG.md +18 -0
  12. package/skills/add-addons-existing-app/SKILL.md +113 -0
  13. package/skills/choose-ecosystem-integrations/SKILL.md +140 -0
  14. package/skills/choose-ecosystem-integrations/references/authentication-providers.md +19 -0
  15. package/skills/choose-ecosystem-integrations/references/data-layer-providers.md +20 -0
  16. package/skills/choose-ecosystem-integrations/references/deployment-targets.md +19 -0
  17. package/skills/create-app-scaffold/SKILL.md +132 -0
  18. package/skills/create-app-scaffold/references/create-flag-compatibility-matrix.md +34 -0
  19. package/skills/create-app-scaffold/references/deployment-providers.md +19 -0
  20. package/skills/create-app-scaffold/references/framework-adapters.md +17 -0
  21. package/skills/create-app-scaffold/references/toolchains.md +17 -0
  22. package/skills/maintain-custom-addons-dev-watch/SKILL.md +118 -0
  23. package/skills/query-docs-library-metadata/SKILL.md +85 -0
  24. package/skills/query-docs-library-metadata/references/discovery-command-output-schemas.md +70 -0
  25. package/CHANGELOG.md +0 -787
  26. package/dist/mcp/api.js +0 -31
  27. package/dist/mcp/tools.js +0 -250
  28. package/dist/mcp/types.js +0 -37
  29. package/dist/mcp.js +0 -181
  30. package/dist/types/mcp/api.d.ts +0 -4
  31. package/dist/types/mcp/tools.d.ts +0 -2
  32. package/dist/types/mcp.d.ts +0 -5
  33. package/playwright-report/index.html +0 -85
  34. package/playwright.config.ts +0 -21
  35. package/src/bin.ts +0 -15
  36. package/src/cli.ts +0 -767
  37. package/src/command-line.ts +0 -473
  38. package/src/dev-watch.ts +0 -564
  39. package/src/file-syncer.ts +0 -263
  40. package/src/index.ts +0 -21
  41. package/src/mcp/api.ts +0 -42
  42. package/src/mcp/tools.ts +0 -323
  43. package/src/mcp/types.ts +0 -46
  44. package/src/mcp.ts +0 -263
  45. package/src/options.ts +0 -234
  46. package/src/types.ts +0 -28
  47. package/src/ui-environment.ts +0 -74
  48. package/src/ui-prompts.ts +0 -355
  49. package/src/utils.ts +0 -30
  50. package/test-results/.last-run.json +0 -4
  51. package/tests/command-line.test.ts +0 -622
  52. package/tests/index.test.ts +0 -9
  53. package/tests/mcp.test.ts +0 -225
  54. package/tests/options.test.ts +0 -216
  55. package/tests/setupVitest.ts +0 -6
  56. package/tests/ui-environment.test.ts +0 -97
  57. package/tests/ui-prompts.test.ts +0 -205
  58. package/tests-e2e/addons-smoke.spec.ts +0 -31
  59. package/tests-e2e/create-smoke.spec.ts +0 -39
  60. package/tests-e2e/helpers.ts +0 -526
  61. package/tests-e2e/matrix-opportunistic.spec.ts +0 -142
  62. package/tests-e2e/router-only-smoke.spec.ts +0 -68
  63. package/tests-e2e/solid-smoke.spec.ts +0 -25
  64. package/tests-e2e/templates-smoke.spec.ts +0 -52
  65. package/tsconfig.json +0 -17
  66. package/vitest.config.js +0 -8
@@ -1,473 +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 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
-
183
- export function validateLegacyCreateFlags(cliOptions: CliOptions): {
184
- warnings: Array<string>
185
- error?: string
186
- } {
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
- }
201
-
202
- if (cliOptions.routerOnly) {
203
- warnings.push(
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.',
205
- )
206
- }
207
-
208
- if (cliOptions.routerOnly && cliOptions.addOns) {
209
- warnings.push(
210
- 'Ignoring --add-ons in router-only compatibility mode.',
211
- )
212
- }
213
-
214
- if (cliOptions.routerOnly && cliOptions.deployment) {
215
- warnings.push(
216
- 'Ignoring --deployment in router-only compatibility mode.',
217
- )
218
- }
219
-
220
- if (cliOptions.routerOnly && cliOptions.starter) {
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.')
226
- }
227
-
228
- if (cliOptions.tailwind === true) {
229
- warnings.push(
230
- 'The --tailwind flag is deprecated and ignored. Tailwind is always enabled in TanStack Start scaffolds.',
231
- )
232
- }
233
-
234
- if (cliOptions.tailwind === false) {
235
- warnings.push(
236
- '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.',
237
- )
238
- }
239
-
240
- if (!legacyTemplate) {
241
- return { warnings }
242
- }
243
-
244
- const template = legacyTemplate
245
-
246
- if (template === 'javascript' || template === 'js' || template === 'jsx') {
247
- return {
248
- warnings,
249
- error:
250
- 'JavaScript/JSX templates are not supported. TanStack Start file-router templates are TypeScript-only.',
251
- }
252
- }
253
-
254
- if (!SUPPORTED_LEGACY_TEMPLATES.has(template)) {
255
- return {
256
- warnings,
257
- error: `Invalid --template value: ${cliOptions.template}. Supported values are: file-router, typescript, tsx.`,
258
- }
259
- }
260
-
261
- warnings.push('The --template flag is deprecated and mapped for compatibility.')
262
-
263
- return { warnings }
264
- }
265
-
266
- export async function normalizeOptions(
267
- cliOptions: CliOptions,
268
- forcedAddOns?: Array<string>,
269
- opts?: {
270
- disableNameCheck?: boolean
271
- forcedDeployment?: string
272
- },
273
- ): Promise<Options | undefined> {
274
- let projectName = (cliOptions.projectName ?? '').trim()
275
- let targetDir: string
276
-
277
- // Handle "." as project name - use current directory
278
- if (projectName === '.') {
279
- projectName = sanitizePackageName(getCurrentDirectoryName())
280
- targetDir = resolve(process.cwd())
281
- } else {
282
- targetDir = resolve(process.cwd(), projectName)
283
- }
284
-
285
- if (!projectName && !opts?.disableNameCheck) {
286
- return undefined
287
- }
288
-
289
- if (projectName) {
290
- const { valid, error } = validateProjectName(projectName)
291
- if (!valid) {
292
- console.error(error)
293
- process.exit(1)
294
- }
295
- }
296
-
297
- // Mode is always file-router (TanStack Start)
298
- let mode = 'file-router'
299
- let routerOnly = !!cliOptions.routerOnly
300
-
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
312
- if (template && template !== 'file-router') {
313
- routerOnly = true
314
- }
315
-
316
- if (!cliOptions.starter && cliOptions.templateId) {
317
- cliOptions.starter = cliOptions.templateId
318
- }
319
-
320
- const starter = !routerOnly && cliOptions.starter
321
- ? await loadStarter(await resolveStarterSpecifier(cliOptions.starter))
322
- : undefined
323
-
324
- // TypeScript and Tailwind are always enabled with TanStack Start
325
- const typescript = true
326
- const tailwind = true
327
-
328
- if (starter) {
329
- cliOptions.framework = starter.framework
330
- mode = starter.mode
331
- }
332
-
333
- const framework = getFrameworkById(cliOptions.framework || 'react')!
334
-
335
- async function selectAddOns() {
336
- // Edge case for Windows Powershell
337
- if (Array.isArray(cliOptions.addOns) && cliOptions.addOns.length === 1) {
338
- const parseSeparatedArgs = cliOptions.addOns[0].split(' ')
339
- if (parseSeparatedArgs.length > 1) {
340
- cliOptions.addOns = parseSeparatedArgs
341
- }
342
- }
343
-
344
- if (
345
- Array.isArray(cliOptions.addOns) ||
346
- starter?.dependsOn ||
347
- forcedAddOns ||
348
- cliOptions.toolchain ||
349
- cliOptions.deployment
350
- ) {
351
- const selectedAddOns = new Set<string>([
352
- ...(routerOnly ? [] : (starter?.dependsOn || [])),
353
- ...(routerOnly ? [] : (forcedAddOns || [])),
354
- ])
355
- if (!routerOnly && cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
356
- for (const a of cliOptions.addOns) {
357
- if (a.toLowerCase() === 'start') {
358
- continue
359
- }
360
- selectedAddOns.add(a)
361
- }
362
- }
363
- if (cliOptions.toolchain) {
364
- selectedAddOns.add(cliOptions.toolchain)
365
- }
366
- if (!routerOnly && cliOptions.deployment) {
367
- selectedAddOns.add(cliOptions.deployment)
368
- }
369
-
370
- if (!routerOnly && !cliOptions.deployment && opts?.forcedDeployment) {
371
- selectedAddOns.add(opts.forcedDeployment)
372
- }
373
-
374
- return await finalizeAddOns(framework, mode, Array.from(selectedAddOns))
375
- }
376
-
377
- return []
378
- }
379
-
380
- const includeExamples = cliOptions.examples ?? !routerOnly
381
- const chosenAddOnsRaw = await selectAddOns()
382
- const chosenAddOns = includeExamples
383
- ? chosenAddOnsRaw
384
- : chosenAddOnsRaw.filter((addOn) => addOn.type !== 'example')
385
-
386
- // Handle add-on configuration option
387
- let addOnOptionsFromCLI = {}
388
- if (cliOptions.addOnConfig) {
389
- try {
390
- addOnOptionsFromCLI = JSON.parse(cliOptions.addOnConfig)
391
- } catch (error) {
392
- console.error('Error parsing add-on config:', error)
393
- process.exit(1)
394
- }
395
- }
396
-
397
- const normalized = {
398
- projectName: projectName,
399
- targetDir,
400
- framework,
401
- mode,
402
- typescript,
403
- tailwind,
404
- packageManager:
405
- cliOptions.packageManager ||
406
- getPackageManager() ||
407
- DEFAULT_PACKAGE_MANAGER,
408
- git: cliOptions.git ?? true,
409
- install: cliOptions.install,
410
- chosenAddOns,
411
- addOnOptions: {
412
- ...populateAddOnOptionsDefaults(chosenAddOns),
413
- ...addOnOptionsFromCLI,
414
- },
415
- starter: starter,
416
- }
417
-
418
- ;(normalized as Options & { includeExamples?: boolean }).includeExamples =
419
- includeExamples
420
- ;(normalized as Options & { envVarValues?: Record<string, string> }).envVarValues =
421
- {}
422
-
423
- return normalized
424
- }
425
-
426
- export function validateDevWatchOptions(cliOptions: CliOptions): {
427
- valid: boolean
428
- error?: string
429
- } {
430
- if (!cliOptions.devWatch) {
431
- return { valid: true }
432
- }
433
-
434
- // Validate watch path exists
435
- const watchPath = resolve(process.cwd(), cliOptions.devWatch)
436
- if (!fs.existsSync(watchPath)) {
437
- return {
438
- valid: false,
439
- error: `Watch path does not exist: ${watchPath}`,
440
- }
441
- }
442
-
443
- // Validate it's a directory
444
- const stats = fs.statSync(watchPath)
445
- if (!stats.isDirectory()) {
446
- return {
447
- valid: false,
448
- error: `Watch path is not a directory: ${watchPath}`,
449
- }
450
- }
451
-
452
- // Ensure target directory is specified
453
- if (!cliOptions.projectName && !cliOptions.targetDir) {
454
- return {
455
- valid: false,
456
- error: 'Project name or target directory is required for dev watch mode',
457
- }
458
- }
459
-
460
- // Check for framework structure
461
- const hasAddOns = fs.existsSync(resolve(watchPath, 'add-ons'))
462
- const hasAssets = fs.existsSync(resolve(watchPath, 'assets'))
463
- const hasFrameworkJson = fs.existsSync(resolve(watchPath, 'framework.json'))
464
-
465
- if (!hasAddOns && !hasAssets && !hasFrameworkJson) {
466
- return {
467
- valid: false,
468
- error: `Watch path does not appear to be a valid framework directory: ${watchPath}`,
469
- }
470
- }
471
-
472
- return { valid: true }
473
- }