@tanstack/cli 0.59.8 → 0.60.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.
@@ -159,20 +159,201 @@ describe('normalizeOptions', () => {
159
159
  expect(options?.framework?.id).toBe('solid')
160
160
  })
161
161
 
162
- it('should default to react-cra if no framework is provided', async () => {
162
+ it('should resolve built-in starter id from registry', async () => {
163
163
  __testRegisterFramework({
164
- id: 'react-cra',
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('should default to react if no framework is provided', async () => {
344
+ __testRegisterFramework({
345
+ id: 'react',
165
346
  name: 'react',
166
347
  })
167
348
  const options = await normalizeOptions({
168
349
  projectName: 'test',
169
350
  })
170
- expect(options?.framework?.id).toBe('react-cra')
351
+ expect(options?.framework?.id).toBe('react')
171
352
  })
172
353
 
173
354
  it('should handle forced addons', async () => {
174
355
  __testRegisterFramework({
175
- id: 'react-cra',
356
+ id: 'react',
176
357
  name: 'react',
177
358
  getAddOns: () => [
178
359
  {
@@ -191,7 +372,7 @@ describe('normalizeOptions', () => {
191
372
  const options = await normalizeOptions(
192
373
  {
193
374
  projectName: 'test',
194
- framework: 'react-cra',
375
+ framework: 'react',
195
376
  },
196
377
  ['foo'],
197
378
  )
@@ -200,7 +381,7 @@ describe('normalizeOptions', () => {
200
381
 
201
382
  it('should handle additional addons from the CLI', async () => {
202
383
  __testRegisterFramework({
203
- id: 'react-cra',
384
+ id: 'react',
204
385
  name: 'react',
205
386
  getAddOns: () => [
206
387
  {
@@ -225,7 +406,7 @@ describe('normalizeOptions', () => {
225
406
  {
226
407
  projectName: 'test',
227
408
  addOns: ['baz'],
228
- framework: 'react-cra',
409
+ framework: 'react',
229
410
  },
230
411
  ['foo'],
231
412
  )
@@ -237,7 +418,7 @@ describe('normalizeOptions', () => {
237
418
 
238
419
  it('should ignore legacy start add-on id from exported commands', async () => {
239
420
  __testRegisterFramework({
240
- id: 'react-cra',
421
+ id: 'react',
241
422
  name: 'react',
242
423
  getAddOns: () => [
243
424
  {
@@ -257,7 +438,7 @@ describe('normalizeOptions', () => {
257
438
  const options = await normalizeOptions({
258
439
  projectName: 'test',
259
440
  addOns: ['start', 'tanstack-query'],
260
- framework: 'react-cra',
441
+ framework: 'react',
261
442
  })
262
443
 
263
444
  expect(options?.chosenAddOns.map((a) => a.id)).toContain('tanstack-query')
@@ -266,7 +447,7 @@ describe('normalizeOptions', () => {
266
447
 
267
448
  it('should handle toolchain as an addon', async () => {
268
449
  __testRegisterFramework({
269
- id: 'react-cra',
450
+ id: 'react',
270
451
  name: 'react',
271
452
  getAddOns: () => [
272
453
  {
@@ -319,7 +500,7 @@ describe('normalizeOptions', () => {
319
500
 
320
501
  it('should ignore add-ons and deployment in router-only mode but keep toolchain', async () => {
321
502
  __testRegisterFramework({
322
- id: 'react-cra',
503
+ id: 'react',
323
504
  name: 'react',
324
505
  getAddOns: () => [
325
506
  {
@@ -345,7 +526,7 @@ describe('normalizeOptions', () => {
345
526
  const options = await normalizeOptions(
346
527
  {
347
528
  projectName: 'test',
348
- framework: 'react-cra',
529
+ framework: 'react',
349
530
  routerOnly: true,
350
531
  addOns: ['form'],
351
532
  deployment: 'nitro',
@@ -360,7 +541,7 @@ describe('normalizeOptions', () => {
360
541
 
361
542
  it('should handle the funky Windows edge case with CLI parsing', async () => {
362
543
  __testRegisterFramework({
363
- id: 'react-cra',
544
+ id: 'react',
364
545
  name: 'react',
365
546
  getAddOns: () => [
366
547
  {
@@ -421,9 +602,9 @@ describe('validateLegacyCreateFlags', () => {
421
602
  expect(result.error).toContain('JavaScript/JSX templates are not supported')
422
603
  })
423
604
 
424
- it('errors for unknown template values', () => {
605
+ it('does not error for non-legacy template values', () => {
425
606
  const result = validateLegacyCreateFlags({ template: 'foo' })
426
- expect(result.error).toContain('Invalid --template value')
607
+ expect(result.error).toBeUndefined()
427
608
  })
428
609
 
429
610
  it('warns for supported deprecated template values', () => {
@@ -431,4 +612,11 @@ describe('validateLegacyCreateFlags', () => {
431
612
  expect(result.error).toBeUndefined()
432
613
  expect(result.warnings[0]).toContain('--template')
433
614
  })
615
+
616
+ it('warns when --starter is used', () => {
617
+ const result = validateLegacyCreateFlags({ starter: 'ecommerce' })
618
+ expect(result.error).toBeUndefined()
619
+ expect(result.warnings[0]).toContain('--starter')
620
+ expect(result.warnings[0]).toContain('deprecated')
621
+ })
434
622
  })
@@ -17,7 +17,7 @@ vi.mock('../src/ui-prompts')
17
17
  beforeEach(() => {
18
18
  __testClearFrameworks()
19
19
  __testRegisterFramework({
20
- id: 'react-cra',
20
+ id: 'react',
21
21
  name: 'react',
22
22
  getAddOns: () => [
23
23
  {
@@ -53,7 +53,7 @@ beforeEach(() => {
53
53
  })
54
54
 
55
55
  const baseCliOptions: CliOptions = {
56
- framework: 'react-cra',
56
+ framework: 'react',
57
57
  addOns: [],
58
58
  toolchain: undefined,
59
59
  projectName: undefined,
@@ -0,0 +1,31 @@
1
+ import { expect, test } from '@playwright/test'
2
+
3
+ import { attachRuntimeGuards, createReactAppFixture } from './helpers'
4
+
5
+ test('@blocking creates app with multiple add-ons and renders demo routes', async ({ page }) => {
6
+ const fixture = await createReactAppFixture({
7
+ appName: 'addons-create-smoke-app',
8
+ addOns: ['shadcn', 'form', 'tanstack-query', 'store'],
9
+ })
10
+ const guards = attachRuntimeGuards(page, fixture.url)
11
+
12
+ try {
13
+ await page.goto(`${fixture.url}/demo/form/simple`)
14
+ await expect(page.getByText('Title', { exact: true })).toBeVisible()
15
+ await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible()
16
+
17
+ await page.goto(`${fixture.url}/demo/tanstack-query`)
18
+ await expect(page.getByRole('heading', { name: /TanStack Query/ })).toBeVisible()
19
+
20
+ await page.goto(`${fixture.url}/demo/store`)
21
+ await expect(page.getByRole('heading', { name: 'Store Example' })).toBeVisible()
22
+ } finally {
23
+ try {
24
+ guards.assertClean()
25
+ } finally {
26
+ guards.dispose()
27
+ await fixture.stop()
28
+ await fixture.cleanup()
29
+ }
30
+ }
31
+ })
@@ -0,0 +1,39 @@
1
+ import { expect, test } from '@playwright/test'
2
+
3
+ import { attachRuntimeGuards, createReactAppFixture } from './helpers'
4
+
5
+ test('@blocking creates a React app and navigates core demo routes', async ({ page }) => {
6
+ const fixture = await createReactAppFixture({
7
+ appName: 'react-smoke-app',
8
+ })
9
+ const guards = attachRuntimeGuards(page, fixture.url)
10
+
11
+ try {
12
+ await page.goto(fixture.url)
13
+ await expect(
14
+ page.getByRole('heading', {
15
+ name: 'Island hours, but for product teams.',
16
+ }),
17
+ ).toBeVisible()
18
+
19
+ await page.getByRole('link', { name: 'Blog' }).click()
20
+ await expect(page).toHaveURL(/\/blog\/?$/)
21
+ await expect(page.getByRole('heading', { name: 'Blog' })).toBeVisible()
22
+
23
+ await page.locator('main article a').first().click()
24
+ await expect(page).toHaveURL(/\/blog\/.+/)
25
+ await expect(page.getByText('Post', { exact: true })).toBeVisible()
26
+
27
+ await page.getByRole('link', { name: 'About' }).click()
28
+ await expect(page).toHaveURL(/\/about\/?$/)
29
+ await expect(page.getByRole('heading', { name: 'Built for shipping fast.' })).toBeVisible()
30
+ } finally {
31
+ try {
32
+ guards.assertClean()
33
+ } finally {
34
+ guards.dispose()
35
+ await fixture.stop()
36
+ await fixture.cleanup()
37
+ }
38
+ }
39
+ })