@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
|
@@ -159,20 +159,201 @@ describe('normalizeOptions', () => {
|
|
|
159
159
|
expect(options?.framework?.id).toBe('solid')
|
|
160
160
|
})
|
|
161
161
|
|
|
162
|
-
it('should
|
|
162
|
+
it('should resolve built-in starter id from registry', async () => {
|
|
163
163
|
__testRegisterFramework({
|
|
164
|
-
id: 'react
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
605
|
+
it('does not error for non-legacy template values', () => {
|
|
425
606
|
const result = validateLegacyCreateFlags({ template: 'foo' })
|
|
426
|
-
expect(result.error).
|
|
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
|
})
|
package/tests/options.test.ts
CHANGED
|
@@ -17,7 +17,7 @@ vi.mock('../src/ui-prompts')
|
|
|
17
17
|
beforeEach(() => {
|
|
18
18
|
__testClearFrameworks()
|
|
19
19
|
__testRegisterFramework({
|
|
20
|
-
id: 'react
|
|
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
|
|
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
|
+
})
|