@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,703 +0,0 @@
1
- import { basename, resolve } from 'node:path'
2
- import { beforeEach, describe, expect, it } from 'vitest'
3
-
4
- import {
5
- normalizeOptions,
6
- validateLegacyCreateFlags,
7
- } from '../src/command-line.js'
8
- import {
9
- sanitizePackageName,
10
- getCurrentDirectoryName,
11
- } from '../src/utils.js'
12
- import {
13
- __testRegisterFramework,
14
- __testClearFrameworks,
15
- } from '@tanstack/create'
16
-
17
- beforeEach(() => {
18
- __testClearFrameworks()
19
- })
20
-
21
- describe('sanitizePackageName', () => {
22
- it('should convert to lowercase', () => {
23
- expect(sanitizePackageName('MyProject')).toBe('myproject')
24
- })
25
-
26
- it('should replace spaces with hyphens', () => {
27
- expect(sanitizePackageName('my project')).toBe('my-project')
28
- })
29
-
30
- it('should replace underscores with hyphens', () => {
31
- expect(sanitizePackageName('my_project')).toBe('my-project')
32
- })
33
-
34
- it('should remove invalid characters', () => {
35
- expect(sanitizePackageName('my@project!')).toBe('myproject')
36
- })
37
-
38
- it('should ensure it starts with a letter', () => {
39
- expect(sanitizePackageName('123project')).toBe('project')
40
- expect(sanitizePackageName('_myproject')).toBe('myproject')
41
- })
42
-
43
- it('should collapse multiple hyphens', () => {
44
- expect(sanitizePackageName('my--project')).toBe('my-project')
45
- })
46
-
47
- it('should remove trailing hyphen', () => {
48
- expect(sanitizePackageName('myproject-')).toBe('myproject')
49
- })
50
- })
51
-
52
- describe('getCurrentDirectoryName', () => {
53
- it('should return the basename of the current working directory', () => {
54
- expect(getCurrentDirectoryName()).toBe(basename(process.cwd()))
55
- })
56
- })
57
-
58
- describe('normalizeOptions', () => {
59
- it('should return undefined if project name is not provided', async () => {
60
- const options = await normalizeOptions({})
61
- expect(options).toBeUndefined()
62
- })
63
-
64
- it('should handle "." as project name by using sanitized current directory name', async () => {
65
- const options = await normalizeOptions({
66
- projectName: '.',
67
- })
68
- const expectedName = sanitizePackageName(getCurrentDirectoryName())
69
- expect(options?.projectName).toBe(expectedName)
70
- expect(options?.targetDir).toBe(resolve(process.cwd()))
71
- })
72
-
73
- it('should always enable typescript (file-router/TanStack Start requires it)', async () => {
74
- const options = await normalizeOptions({
75
- projectName: 'test',
76
- })
77
- expect(options?.typescript).toBe(true)
78
- expect(options?.mode).toBe('file-router')
79
- })
80
-
81
- it('tailwind is always enabled', async () => {
82
- const options = await normalizeOptions({
83
- projectName: 'test',
84
- })
85
- expect(options?.tailwind).toBe(true)
86
-
87
- const solidOptions = await normalizeOptions({
88
- projectName: 'test',
89
- framework: 'solid',
90
- })
91
- expect(solidOptions?.tailwind).toBe(true)
92
- })
93
-
94
- it('defaults git initialization to enabled', async () => {
95
- const options = await normalizeOptions({
96
- projectName: 'test',
97
- })
98
-
99
- expect(options?.git).toBe(true)
100
- })
101
-
102
- it('respects explicit --no-git option', async () => {
103
- const options = await normalizeOptions({
104
- projectName: 'test',
105
- git: false,
106
- })
107
-
108
- expect(options?.git).toBe(false)
109
- })
110
-
111
- it('should handle a starter url', async () => {
112
- __testRegisterFramework({
113
- id: 'solid',
114
- name: 'Solid',
115
- getAddOns: () => [
116
- {
117
- id: 'nitro',
118
- name: 'nitro',
119
- modes: ['file-router'],
120
- default: true,
121
- },
122
- ],
123
- supportedModes: {
124
- 'code-router': {
125
- displayName: 'Code Router',
126
- description: 'TanStack Router using code to define the routes',
127
- forceTypescript: false,
128
- },
129
- 'file-router': {
130
- displayName: 'File Router',
131
- description: 'TanStack Router using files to define the routes',
132
- forceTypescript: true,
133
- },
134
- },
135
- })
136
- fetch.mockResponseOnce(
137
- JSON.stringify({
138
- id: 'https://github.com/cta-dev/cta-starter-solid',
139
- typescript: false,
140
- framework: 'solid',
141
- mode: 'file-router',
142
- type: 'starter',
143
- description: 'A starter for Solid',
144
- name: 'My Solid Starter',
145
- dependsOn: [],
146
- files: {},
147
- deletedFiles: [],
148
- }),
149
- )
150
-
151
- const options = await normalizeOptions({
152
- projectName: 'test',
153
- starter: 'https://github.com/cta-dev/cta-starter-solid',
154
- deployment: 'nitro',
155
- })
156
- expect(options?.mode).toBe('file-router')
157
- expect(options?.tailwind).toBe(true)
158
- expect(options?.typescript).toBe(true)
159
- expect(options?.framework?.id).toBe('solid')
160
- })
161
-
162
- it('should resolve built-in starter id from registry', async () => {
163
- __testRegisterFramework({
164
- id: 'react',
165
- name: 'React',
166
- getAddOns: () => [],
167
- supportedModes: {
168
- 'file-router': {
169
- displayName: 'File Router',
170
- description: 'TanStack Router using files to define the routes',
171
- forceTypescript: true,
172
- },
173
- },
174
- })
175
-
176
- const originalRegistry = process.env.CTA_REGISTRY
177
- process.env.CTA_REGISTRY = 'https://registry.example/registry.json'
178
-
179
- fetch
180
- .mockResponseOnce(
181
- JSON.stringify({
182
- starters: [
183
- {
184
- name: 'Ecommerce',
185
- description: 'Ecommerce base',
186
- url: './ecommerce/template.json',
187
- mode: 'file-router',
188
- framework: 'react',
189
- },
190
- ],
191
- }),
192
- )
193
- .mockResponseOnce(
194
- JSON.stringify({
195
- id: 'ecommerce',
196
- typescript: true,
197
- framework: 'react',
198
- mode: 'file-router',
199
- type: 'starter',
200
- description: 'Ecommerce base',
201
- name: 'Ecommerce',
202
- dependsOn: [],
203
- files: {},
204
- deletedFiles: [],
205
- }),
206
- )
207
-
208
- try {
209
- const options = await normalizeOptions({
210
- projectName: 'test',
211
- starter: 'ecommerce',
212
- })
213
-
214
- expect(options?.framework?.id).toBe('react')
215
- expect(options?.starter?.id).toBe(
216
- 'https://registry.example/ecommerce/template.json',
217
- )
218
- } finally {
219
- process.env.CTA_REGISTRY = originalRegistry
220
- }
221
- })
222
-
223
- it('should map --template-id to starter resolution', async () => {
224
- __testRegisterFramework({
225
- id: 'react',
226
- name: 'React',
227
- getAddOns: () => [],
228
- supportedModes: {
229
- 'file-router': {
230
- displayName: 'File Router',
231
- description: 'TanStack Router using files to define the routes',
232
- forceTypescript: true,
233
- },
234
- },
235
- })
236
-
237
- const originalRegistry = process.env.CTA_REGISTRY
238
- process.env.CTA_REGISTRY = 'https://registry.example/registry.json'
239
-
240
- fetch
241
- .mockResponseOnce(
242
- JSON.stringify({
243
- templates: [
244
- {
245
- name: 'Resume',
246
- description: 'Resume template',
247
- url: './resume/template.json',
248
- mode: 'file-router',
249
- framework: 'react',
250
- },
251
- ],
252
- }),
253
- )
254
- .mockResponseOnce(
255
- JSON.stringify({
256
- id: 'resume',
257
- typescript: true,
258
- framework: 'react',
259
- mode: 'file-router',
260
- type: 'starter',
261
- description: 'Resume template',
262
- name: 'Resume',
263
- dependsOn: [],
264
- files: {},
265
- deletedFiles: [],
266
- }),
267
- )
268
-
269
- try {
270
- const options = await normalizeOptions({
271
- projectName: 'test',
272
- templateId: 'resume',
273
- })
274
-
275
- expect(options?.starter?.id).toBe(
276
- 'https://registry.example/resume/template.json',
277
- )
278
- } finally {
279
- process.env.CTA_REGISTRY = originalRegistry
280
- }
281
- })
282
-
283
- it('should resolve --template as a template id from registry', async () => {
284
- __testRegisterFramework({
285
- id: 'react',
286
- name: 'React',
287
- getAddOns: () => [],
288
- supportedModes: {
289
- 'file-router': {
290
- displayName: 'File Router',
291
- description: 'TanStack Router using files to define the routes',
292
- forceTypescript: true,
293
- },
294
- },
295
- })
296
-
297
- const originalRegistry = process.env.CTA_REGISTRY
298
- process.env.CTA_REGISTRY = 'https://registry.example/registry.json'
299
-
300
- fetch
301
- .mockResponseOnce(
302
- JSON.stringify({
303
- templates: [
304
- {
305
- name: 'Ecommerce',
306
- description: 'Ecommerce template',
307
- url: './ecommerce/template.json',
308
- mode: 'file-router',
309
- framework: 'react',
310
- },
311
- ],
312
- }),
313
- )
314
- .mockResponseOnce(
315
- JSON.stringify({
316
- id: 'ecommerce',
317
- typescript: true,
318
- framework: 'react',
319
- mode: 'file-router',
320
- type: 'starter',
321
- description: 'Ecommerce template',
322
- name: 'Ecommerce',
323
- dependsOn: [],
324
- files: {},
325
- deletedFiles: [],
326
- }),
327
- )
328
-
329
- try {
330
- const options = await normalizeOptions({
331
- projectName: 'test',
332
- template: 'ecommerce',
333
- })
334
-
335
- expect(options?.starter?.id).toBe(
336
- 'https://registry.example/ecommerce/template.json',
337
- )
338
- } finally {
339
- process.env.CTA_REGISTRY = originalRegistry
340
- }
341
- })
342
-
343
- it('prefers framework-matching template ids from registry', async () => {
344
- __testRegisterFramework({
345
- id: 'react',
346
- name: 'React',
347
- getAddOns: () => [],
348
- supportedModes: {
349
- 'file-router': {
350
- displayName: 'File Router',
351
- description: 'TanStack Router using files to define the routes',
352
- forceTypescript: true,
353
- },
354
- },
355
- })
356
- __testRegisterFramework({
357
- id: 'solid',
358
- name: 'Solid',
359
- getAddOns: () => [],
360
- supportedModes: {
361
- 'file-router': {
362
- displayName: 'File Router',
363
- description: 'TanStack Router using files to define the routes',
364
- forceTypescript: true,
365
- },
366
- },
367
- })
368
-
369
- const originalRegistry = process.env.CTA_REGISTRY
370
- process.env.CTA_REGISTRY = 'https://registry.example/registry.json'
371
-
372
- fetch
373
- .mockResponseOnce(
374
- JSON.stringify({
375
- templates: [
376
- {
377
- name: 'Blog',
378
- description: 'React blog template',
379
- url: './react/blog/template.json',
380
- mode: 'file-router',
381
- framework: 'react',
382
- },
383
- {
384
- name: 'Blog',
385
- description: 'Solid blog template',
386
- url: './solid/blog/template.json',
387
- mode: 'file-router',
388
- framework: 'solid',
389
- },
390
- ],
391
- }),
392
- )
393
- .mockResponseOnce(
394
- JSON.stringify({
395
- id: 'blog',
396
- typescript: true,
397
- framework: 'solid',
398
- mode: 'file-router',
399
- type: 'starter',
400
- description: 'Solid blog template',
401
- name: 'Blog',
402
- dependsOn: [],
403
- files: {},
404
- deletedFiles: [],
405
- }),
406
- )
407
-
408
- try {
409
- const options = await normalizeOptions({
410
- projectName: 'test',
411
- framework: 'solid',
412
- template: 'blog',
413
- })
414
-
415
- expect(options?.framework?.id).toBe('solid')
416
- expect(options?.starter?.id).toBe(
417
- 'https://registry.example/solid/blog/template.json',
418
- )
419
- } finally {
420
- process.env.CTA_REGISTRY = originalRegistry
421
- }
422
- })
423
-
424
- it('should default to react if no framework is provided', async () => {
425
- __testRegisterFramework({
426
- id: 'react',
427
- name: 'react',
428
- })
429
- const options = await normalizeOptions({
430
- projectName: 'test',
431
- })
432
- expect(options?.framework?.id).toBe('react')
433
- })
434
-
435
- it('should handle forced addons', async () => {
436
- __testRegisterFramework({
437
- id: 'react',
438
- name: 'react',
439
- getAddOns: () => [
440
- {
441
- id: 'foo',
442
- name: 'foobar',
443
- modes: ['file-router'],
444
- },
445
- {
446
- id: 'nitro',
447
- name: 'nitro',
448
- modes: ['file-router'],
449
- default: true,
450
- },
451
- ],
452
- })
453
- const options = await normalizeOptions(
454
- {
455
- projectName: 'test',
456
- framework: 'react',
457
- },
458
- ['foo'],
459
- )
460
- expect(options?.chosenAddOns.map((a) => a.id).includes('foo')).toBe(true)
461
- })
462
-
463
- it('should handle additional addons from the CLI', async () => {
464
- __testRegisterFramework({
465
- id: 'react',
466
- name: 'react',
467
- getAddOns: () => [
468
- {
469
- id: 'foo',
470
- name: 'foobar',
471
- modes: ['file-router'],
472
- },
473
- {
474
- id: 'baz',
475
- name: 'baz',
476
- modes: ['file-router'],
477
- },
478
- {
479
- id: 'nitro',
480
- name: 'nitro',
481
- modes: ['file-router'],
482
- default: true,
483
- },
484
- ],
485
- })
486
- const options = await normalizeOptions(
487
- {
488
- projectName: 'test',
489
- addOns: ['baz'],
490
- framework: 'react',
491
- },
492
- ['foo'],
493
- )
494
- expect(options?.chosenAddOns.map((a) => a.id).includes('foo')).toBe(true)
495
- expect(options?.chosenAddOns.map((a) => a.id).includes('baz')).toBe(true)
496
- expect(options?.tailwind).toBe(true)
497
- expect(options?.typescript).toBe(true)
498
- })
499
-
500
- it('should ignore legacy start add-on id from exported commands', async () => {
501
- __testRegisterFramework({
502
- id: 'react',
503
- name: 'react',
504
- getAddOns: () => [
505
- {
506
- id: 'tanstack-query',
507
- name: 'TanStack Query',
508
- modes: ['file-router'],
509
- },
510
- {
511
- id: 'nitro',
512
- name: 'nitro',
513
- modes: ['file-router'],
514
- default: true,
515
- },
516
- ],
517
- })
518
-
519
- const options = await normalizeOptions({
520
- projectName: 'test',
521
- addOns: ['start', 'tanstack-query'],
522
- framework: 'react',
523
- })
524
-
525
- expect(options?.chosenAddOns.map((a) => a.id)).toContain('tanstack-query')
526
- expect(options?.chosenAddOns.map((a) => a.id)).not.toContain('start')
527
- })
528
-
529
- it('should handle toolchain as an addon', async () => {
530
- __testRegisterFramework({
531
- id: 'react',
532
- name: 'react',
533
- getAddOns: () => [
534
- {
535
- id: 'biome',
536
- name: 'Biome',
537
- modes: ['file-router', 'code-router'],
538
- },
539
- {
540
- id: 'nitro',
541
- name: 'nitro',
542
- modes: ['file-router', 'code-router'],
543
- default: true,
544
- },
545
- ],
546
- })
547
- const options = await normalizeOptions({
548
- projectName: 'test',
549
- toolchain: 'biome',
550
- })
551
- expect(options?.chosenAddOns.map((a) => a.id).includes('biome')).toBe(true)
552
- expect(options?.tailwind).toBe(true)
553
- expect(options?.typescript).toBe(true)
554
- })
555
-
556
- it('should keep file-router mode in router-only compatibility mode', async () => {
557
- const options = await normalizeOptions({
558
- projectName: 'test',
559
- routerOnly: true,
560
- })
561
-
562
- expect(options?.mode).toBe('file-router')
563
- })
564
-
565
- it('includes examples by default in non-router-only mode', async () => {
566
- const options = await normalizeOptions({
567
- projectName: 'test',
568
- })
569
-
570
- expect((options as any)?.includeExamples).toBe(true)
571
- })
572
-
573
- it('supports disabling examples from the CLI', async () => {
574
- const options = await normalizeOptions({
575
- projectName: 'test',
576
- examples: false,
577
- })
578
-
579
- expect((options as any)?.includeExamples).toBe(false)
580
- })
581
-
582
- it('should ignore add-ons and deployment in router-only mode but keep toolchain', async () => {
583
- __testRegisterFramework({
584
- id: 'react',
585
- name: 'react',
586
- getAddOns: () => [
587
- {
588
- id: 'form',
589
- name: 'Form',
590
- modes: ['file-router'],
591
- },
592
- {
593
- id: 'nitro',
594
- name: 'nitro',
595
- modes: ['file-router'],
596
- type: 'deployment',
597
- },
598
- {
599
- id: 'biome',
600
- name: 'Biome',
601
- modes: ['file-router'],
602
- type: 'toolchain',
603
- },
604
- ],
605
- })
606
-
607
- const options = await normalizeOptions(
608
- {
609
- projectName: 'test',
610
- framework: 'react',
611
- routerOnly: true,
612
- addOns: ['form'],
613
- deployment: 'nitro',
614
- toolchain: 'biome',
615
- },
616
- ['form'],
617
- { forcedDeployment: 'nitro' },
618
- )
619
-
620
- expect(options?.chosenAddOns.map((a) => a.id)).toEqual(['biome'])
621
- })
622
-
623
- it('should handle the funky Windows edge case with CLI parsing', async () => {
624
- __testRegisterFramework({
625
- id: 'react',
626
- name: 'react',
627
- getAddOns: () => [
628
- {
629
- id: 'foo',
630
- name: 'foobar',
631
- modes: ['file-router', 'code-router'],
632
- },
633
- {
634
- id: 'baz',
635
- name: 'baz',
636
- modes: ['file-router', 'code-router'],
637
- },
638
- {
639
- id: 'nitro',
640
- name: 'nitro',
641
- modes: ['file-router', 'code-router'],
642
- default: true,
643
- },
644
- ],
645
- })
646
- const options = await normalizeOptions({
647
- projectName: 'test',
648
- addOns: ['baz foo'],
649
- })
650
- expect(options?.chosenAddOns.map((a) => a.id).includes('foo')).toBe(true)
651
- expect(options?.chosenAddOns.map((a) => a.id).includes('baz')).toBe(true)
652
- })
653
- })
654
-
655
- describe('validateLegacyCreateFlags', () => {
656
- it('returns no warnings or errors without legacy flags', () => {
657
- const result = validateLegacyCreateFlags({})
658
- expect(result.warnings).toEqual([])
659
- expect(result.error).toBeUndefined()
660
- })
661
-
662
- it('warns when --router-only is used', () => {
663
- const result = validateLegacyCreateFlags({ routerOnly: true })
664
- expect(result.error).toBeUndefined()
665
- expect(result.warnings[0]).toContain('--router-only')
666
- })
667
-
668
- it('warns when --tailwind is used', () => {
669
- const result = validateLegacyCreateFlags({ tailwind: true })
670
- expect(result.error).toBeUndefined()
671
- expect(result.warnings[0]).toContain('--tailwind')
672
- })
673
-
674
- it('warns heavily when --no-tailwind is used', () => {
675
- const result = validateLegacyCreateFlags({ tailwind: false })
676
- expect(result.error).toBeUndefined()
677
- expect(result.warnings[0]).toContain('--no-tailwind')
678
- expect(result.warnings[0]).toContain('intentionally unsupported')
679
- })
680
-
681
- it('errors for JavaScript templates', () => {
682
- const result = validateLegacyCreateFlags({ template: 'javascript' })
683
- expect(result.error).toContain('JavaScript/JSX templates are not supported')
684
- })
685
-
686
- it('does not error for non-legacy template values', () => {
687
- const result = validateLegacyCreateFlags({ template: 'foo' })
688
- expect(result.error).toBeUndefined()
689
- })
690
-
691
- it('warns for supported deprecated template values', () => {
692
- const result = validateLegacyCreateFlags({ template: 'tsx' })
693
- expect(result.error).toBeUndefined()
694
- expect(result.warnings[0]).toContain('--template')
695
- })
696
-
697
- it('warns when --starter is used', () => {
698
- const result = validateLegacyCreateFlags({ starter: 'ecommerce' })
699
- expect(result.error).toBeUndefined()
700
- expect(result.warnings[0]).toContain('--starter')
701
- expect(result.warnings[0]).toContain('deprecated')
702
- })
703
- })
@@ -1,9 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
-
3
- import { cli } from '../src/index.js'
4
-
5
- describe('cli', () => {
6
- it('should call the cli with the correct arguments', async () => {
7
- expect(cli).toBeDefined()
8
- })
9
- })