@tanstack/cli 0.0.7 → 0.48.2

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 (83) hide show
  1. package/dist/bin.js +7 -0
  2. package/dist/cli.js +481 -0
  3. package/dist/command-line.js +174 -0
  4. package/dist/dev-watch.js +290 -0
  5. package/dist/file-syncer.js +148 -0
  6. package/dist/index.js +1 -0
  7. package/dist/mcp/api.js +31 -0
  8. package/dist/mcp/tools.js +250 -0
  9. package/dist/mcp/types.js +37 -0
  10. package/dist/mcp.js +121 -0
  11. package/dist/options.js +162 -0
  12. package/dist/types/bin.d.ts +2 -0
  13. package/dist/types/cli.d.ts +16 -0
  14. package/dist/types/command-line.d.ts +10 -0
  15. package/dist/types/dev-watch.d.ts +27 -0
  16. package/dist/types/file-syncer.d.ts +18 -0
  17. package/dist/types/index.d.ts +1 -0
  18. package/dist/types/mcp/api.d.ts +4 -0
  19. package/dist/types/mcp/tools.d.ts +2 -0
  20. package/dist/types/mcp/types.d.ts +217 -0
  21. package/dist/types/mcp.d.ts +6 -0
  22. package/dist/types/options.d.ts +8 -0
  23. package/dist/types/types.d.ts +25 -0
  24. package/dist/types/ui-environment.d.ts +2 -0
  25. package/dist/types/ui-prompts.d.ts +12 -0
  26. package/dist/types/utils.d.ts +8 -0
  27. package/dist/types.js +1 -0
  28. package/dist/ui-environment.js +52 -0
  29. package/dist/ui-prompts.js +244 -0
  30. package/dist/utils.js +30 -0
  31. package/package.json +46 -46
  32. package/src/bin.ts +6 -93
  33. package/src/cli.ts +692 -0
  34. package/src/command-line.ts +236 -0
  35. package/src/dev-watch.ts +430 -0
  36. package/src/file-syncer.ts +205 -0
  37. package/src/index.ts +1 -85
  38. package/src/mcp.ts +190 -0
  39. package/src/options.ts +260 -0
  40. package/src/types.ts +27 -0
  41. package/src/ui-environment.ts +74 -0
  42. package/src/ui-prompts.ts +322 -0
  43. package/src/utils.ts +38 -0
  44. package/tests/command-line.test.ts +304 -0
  45. package/tests/index.test.ts +9 -0
  46. package/tests/mcp.test.ts +225 -0
  47. package/tests/options.test.ts +304 -0
  48. package/tests/setupVitest.ts +6 -0
  49. package/tests/ui-environment.test.ts +97 -0
  50. package/tests/ui-prompts.test.ts +238 -0
  51. package/tsconfig.json +17 -0
  52. package/vitest.config.js +7 -0
  53. package/dist/bin.cjs +0 -761
  54. package/dist/bin.d.cts +0 -1
  55. package/dist/bin.d.mts +0 -1
  56. package/dist/bin.mjs +0 -760
  57. package/dist/index.cjs +0 -36
  58. package/dist/index.d.cts +0 -1172
  59. package/dist/index.d.mts +0 -1172
  60. package/dist/index.mjs +0 -3
  61. package/dist/template-CkAkdP8n.mjs +0 -2545
  62. package/dist/template-Cup47s9h.cjs +0 -2783
  63. package/src/api/fetch.test.ts +0 -114
  64. package/src/api/fetch.ts +0 -249
  65. package/src/cache/index.ts +0 -89
  66. package/src/commands/create.ts +0 -463
  67. package/src/commands/mcp.test.ts +0 -152
  68. package/src/commands/mcp.ts +0 -203
  69. package/src/engine/compile-with-addons.test.ts +0 -302
  70. package/src/engine/compile.test.ts +0 -404
  71. package/src/engine/compile.ts +0 -551
  72. package/src/engine/config-file.test.ts +0 -118
  73. package/src/engine/config-file.ts +0 -61
  74. package/src/engine/custom-addons/integration.ts +0 -323
  75. package/src/engine/custom-addons/shared.test.ts +0 -98
  76. package/src/engine/custom-addons/shared.ts +0 -281
  77. package/src/engine/custom-addons/template.test.ts +0 -288
  78. package/src/engine/custom-addons/template.ts +0 -124
  79. package/src/engine/template.test.ts +0 -256
  80. package/src/engine/template.ts +0 -269
  81. package/src/engine/types.ts +0 -336
  82. package/src/parse-gitignore.d.ts +0 -5
  83. package/src/templates/base.ts +0 -891
@@ -1,551 +0,0 @@
1
- import { getBaseFiles, getBaseFilesWithAttribution } from '../templates/base.js'
2
- import { processTemplateFile } from './template.js'
3
- import type {
4
- AttributedCompileOutput,
5
- CompileOptions,
6
- CompileOutput,
7
- EnvVar,
8
- IntegrationCompiled,
9
- IntegrationPhase,
10
- LineAttribution,
11
- } from './types.js'
12
-
13
- /**
14
- * Merge package contributions from integrations
15
- */
16
- function mergePackages(
17
- target: CompileOutput['packages'],
18
- source: IntegrationCompiled['packageAdditions'],
19
- ): void {
20
- if (!source) return
21
-
22
- if (source.dependencies) {
23
- target.dependencies = { ...target.dependencies, ...source.dependencies }
24
- }
25
- if (source.devDependencies) {
26
- target.devDependencies = {
27
- ...target.devDependencies,
28
- ...source.devDependencies,
29
- }
30
- }
31
- if (source.scripts) {
32
- target.scripts = { ...target.scripts, ...source.scripts }
33
- }
34
- }
35
-
36
- /**
37
- * Track package attribution for package.json line coloring
38
- */
39
- type PackageAttribution = Map<string, string> // package name -> integration id
40
-
41
- function mergePackagesWithAttribution(
42
- target: CompileOutput['packages'],
43
- source: IntegrationCompiled['packageAdditions'],
44
- integrationId: string,
45
- attribution: {
46
- dependencies: PackageAttribution
47
- devDependencies: PackageAttribution
48
- scripts: PackageAttribution
49
- },
50
- ): void {
51
- if (!source) return
52
-
53
- if (source.dependencies) {
54
- for (const pkg of Object.keys(source.dependencies)) {
55
- attribution.dependencies.set(pkg, integrationId)
56
- }
57
- target.dependencies = { ...target.dependencies, ...source.dependencies }
58
- }
59
- if (source.devDependencies) {
60
- for (const pkg of Object.keys(source.devDependencies)) {
61
- attribution.devDependencies.set(pkg, integrationId)
62
- }
63
- target.devDependencies = {
64
- ...target.devDependencies,
65
- ...source.devDependencies,
66
- }
67
- }
68
- if (source.scripts) {
69
- for (const script of Object.keys(source.scripts)) {
70
- attribution.scripts.set(script, integrationId)
71
- }
72
- target.scripts = { ...target.scripts, ...source.scripts }
73
- }
74
- }
75
-
76
- /**
77
- * Process all files from an integration
78
- */
79
- function processIntegrationFiles(
80
- integration: IntegrationCompiled,
81
- options: CompileOptions,
82
- files: Map<string, { content: string; integrationId: string }>,
83
- appendFiles: Map<string, Array<{ content: string; integrationId: string }>>,
84
- ): void {
85
- for (const [filePath, content] of Object.entries(integration.files)) {
86
- const processed = processTemplateFile(filePath, content, options)
87
-
88
- if (!processed) continue
89
-
90
- if (processed.append) {
91
- // Queue for appending
92
- if (!appendFiles.has(processed.path)) {
93
- appendFiles.set(processed.path, [])
94
- }
95
- appendFiles.get(processed.path)!.push({
96
- content: processed.content,
97
- integrationId: integration.id,
98
- })
99
- } else {
100
- // Overwrite (later integrations win)
101
- files.set(processed.path, {
102
- content: processed.content,
103
- integrationId: integration.id,
104
- })
105
- }
106
- }
107
- }
108
-
109
- /**
110
- * Build the package.json content
111
- */
112
- function buildPackageJson(
113
- options: CompileOptions,
114
- packages: CompileOutput['packages'],
115
- ): string {
116
- // Header is shown when there are integrations and tailwind is enabled
117
- const hasHeader = options.chosenIntegrations.length > 0 && options.tailwind
118
-
119
- const pkg = {
120
- name: options.projectName,
121
- private: true,
122
- type: 'module',
123
- scripts: {
124
- dev: 'vite dev --port 3000',
125
- build: 'vite build',
126
- start: 'node .output/server/index.mjs',
127
- ...packages.scripts,
128
- },
129
- dependencies: {
130
- '@tanstack/react-router': '^1.132.0',
131
- '@tanstack/react-router-devtools': '^1.132.0',
132
- '@tanstack/react-start': '^1.132.0',
133
- react: '^19.2.0',
134
- 'react-dom': '^19.2.0',
135
- 'vite-tsconfig-paths': '^5.1.4',
136
- ...(hasHeader ? { 'lucide-react': '^0.468.0' } : {}),
137
- ...packages.dependencies,
138
- },
139
- devDependencies: {
140
- '@vitejs/plugin-react': '^4.4.1',
141
- vite: '^7.0.0',
142
- ...(options.typescript
143
- ? {
144
- '@types/react': '^19.2.0',
145
- '@types/react-dom': '^19.2.0',
146
- typescript: '^5.7.0',
147
- }
148
- : {}),
149
- ...(options.tailwind
150
- ? {
151
- '@tailwindcss/vite': '^4.0.0',
152
- tailwindcss: '^4.0.0',
153
- }
154
- : {}),
155
- ...packages.devDependencies,
156
- },
157
- }
158
-
159
- return JSON.stringify(pkg, null, 2)
160
- }
161
-
162
- /**
163
- * Compile a project from options
164
- */
165
- export function compile(options: CompileOptions): CompileOutput {
166
- const files = new Map<string, { content: string; integrationId: string }>()
167
- const appendFiles = new Map<
168
- string,
169
- Array<{ content: string; integrationId: string }>
170
- >()
171
- const packages: CompileOutput['packages'] = {
172
- dependencies: {},
173
- devDependencies: {},
174
- scripts: {},
175
- }
176
- const envVars: Array<EnvVar> = []
177
- const warnings: Array<string> = []
178
-
179
- // Add base template files first
180
- const baseFiles = getBaseFiles(options)
181
- for (const [path, content] of Object.entries(baseFiles)) {
182
- files.set(path, { content, integrationId: 'base' })
183
- }
184
-
185
- // Sort integrations by phase and priority
186
- const sortedIntegrations = [...options.chosenIntegrations].sort((a, b) => {
187
- const phaseOrder: Record<IntegrationPhase, number> = { setup: 0, integration: 1, example: 2 }
188
- const phaseA = phaseOrder[a.phase]
189
- const phaseB = phaseOrder[b.phase]
190
-
191
- if (phaseA !== phaseB) return phaseA - phaseB
192
- return (a.priority ?? 100) - (b.priority ?? 100)
193
- })
194
-
195
- // Process each integration
196
- for (const integration of sortedIntegrations) {
197
- // Process files
198
- processIntegrationFiles(integration, options, files, appendFiles)
199
-
200
- // Merge packages
201
- mergePackages(packages, integration.packageAdditions)
202
-
203
- // Collect env vars
204
- if (integration.envVars) {
205
- envVars.push(...integration.envVars)
206
- }
207
-
208
- // Collect warnings
209
- if (integration.warning) {
210
- warnings.push(`${integration.name}: ${integration.warning}`)
211
- }
212
- }
213
-
214
- // Apply appended content
215
- for (const [path, appends] of appendFiles) {
216
- const existing = files.get(path)
217
- if (existing) {
218
- const appendContent = appends.map((a) => a.content).join('\n')
219
- existing.content = existing.content + '\n' + appendContent
220
- } else {
221
- // File doesn't exist yet, create it from appends
222
- files.set(path, {
223
- content: appends.map((a) => a.content).join('\n'),
224
- integrationId: appends[0]?.integrationId ?? 'base',
225
- })
226
- }
227
- }
228
-
229
- // Note: Custom templates don't add files directly - they just specify which integrations to use
230
- // The template's integration list should already be resolved into chosenIntegrations by the caller
231
-
232
- // Build final files map
233
- const outputFiles: Record<string, string> = {}
234
- for (const [path, { content }] of files) {
235
- outputFiles[path] = content
236
- }
237
-
238
- // Add package.json
239
- outputFiles['package.json'] = buildPackageJson(options, packages)
240
-
241
- // Deduplicate env vars
242
- const seenEnvVars = new Set<string>()
243
- const uniqueEnvVars = envVars.filter((v) => {
244
- if (seenEnvVars.has(v.name)) return false
245
- seenEnvVars.add(v.name)
246
- return true
247
- })
248
-
249
- return {
250
- files: outputFiles,
251
- packages,
252
- envVars: uniqueEnvVars,
253
- warnings,
254
- }
255
- }
256
-
257
- /**
258
- * Compile with line-by-line attribution tracking
259
- */
260
- export function compileWithAttribution(
261
- options: CompileOptions,
262
- ): AttributedCompileOutput {
263
- const files = new Map<string, { content: string; integrationId: string }>()
264
- const appendFiles = new Map<
265
- string,
266
- Array<{ content: string; integrationId: string }>
267
- >()
268
- const packages: CompileOutput['packages'] = {
269
- dependencies: {},
270
- devDependencies: {},
271
- scripts: {},
272
- }
273
- const packageAttribution = {
274
- dependencies: new Map<string, string>(),
275
- devDependencies: new Map<string, string>(),
276
- scripts: new Map<string, string>(),
277
- }
278
- const envVars: Array<EnvVar & { integrationId: string }> = []
279
- const warnings: Array<string> = []
280
-
281
- // Track which integration contributed each file
282
- const fileOwnership = new Map<string, string>()
283
-
284
- // Add base template files first (with hook attribution)
285
- const { files: baseFiles, attributions: baseAttributions } =
286
- getBaseFilesWithAttribution(options)
287
- for (const [path, content] of Object.entries(baseFiles)) {
288
- files.set(path, { content, integrationId: 'base' })
289
- fileOwnership.set(path, 'base')
290
- }
291
-
292
- // Store base file attributions for later
293
- const hookAttributions = new Map<
294
- string,
295
- Array<{ line: number; integrationId: string }>
296
- >()
297
- for (const [path, attrs] of Object.entries(baseAttributions)) {
298
- hookAttributions.set(path, attrs)
299
- }
300
-
301
- // Sort integrations by phase and priority
302
- const sortedIntegrations = [...options.chosenIntegrations].sort((a, b) => {
303
- const phaseOrder: Record<IntegrationPhase, number> = { setup: 0, integration: 1, example: 2 }
304
- const phaseA = phaseOrder[a.phase]
305
- const phaseB = phaseOrder[b.phase]
306
-
307
- if (phaseA !== phaseB) return phaseA - phaseB
308
- return (a.priority ?? 100) - (b.priority ?? 100)
309
- })
310
-
311
- // Create integration name lookup
312
- const integrationNames = new Map<string, string>()
313
- integrationNames.set('base', 'Base Template')
314
- if (options.customTemplate) {
315
- integrationNames.set(options.customTemplate.id, options.customTemplate.name)
316
- }
317
- for (const integration of sortedIntegrations) {
318
- integrationNames.set(integration.id, integration.name)
319
- }
320
-
321
- // Process each integration
322
- for (const integration of sortedIntegrations) {
323
- for (const [filePath, content] of Object.entries(integration.files)) {
324
- const processed = processTemplateFile(filePath, content, options)
325
-
326
- if (!processed) continue
327
-
328
- if (processed.append) {
329
- if (!appendFiles.has(processed.path)) {
330
- appendFiles.set(processed.path, [])
331
- }
332
- appendFiles.get(processed.path)!.push({
333
- content: processed.content,
334
- integrationId: integration.id,
335
- })
336
- } else {
337
- files.set(processed.path, {
338
- content: processed.content,
339
- integrationId: integration.id,
340
- })
341
- fileOwnership.set(processed.path, integration.id)
342
- }
343
- }
344
-
345
- mergePackagesWithAttribution(
346
- packages,
347
- integration.packageAdditions,
348
- integration.id,
349
- packageAttribution,
350
- )
351
-
352
- if (integration.envVars) {
353
- for (const envVar of integration.envVars) {
354
- envVars.push({ ...envVar, integrationId: integration.id })
355
- }
356
- }
357
-
358
- if (integration.warning) {
359
- warnings.push(`${integration.name}: ${integration.warning}`)
360
- }
361
- }
362
-
363
- // Apply appended content with tracking
364
- const appendOwnership = new Map<string, Map<number, string>>()
365
-
366
- for (const [path, appends] of appendFiles) {
367
- const existing = files.get(path)
368
- if (existing) {
369
- const existingLines = existing.content.split('\n').length
370
- const lineMap = new Map<number, string>()
371
-
372
- let currentLine = existingLines + 1
373
- for (const append of appends) {
374
- const appendLines = append.content.split('\n').length
375
- for (let i = 0; i < appendLines; i++) {
376
- lineMap.set(currentLine + i, append.integrationId)
377
- }
378
- currentLine += appendLines + 1 // +1 for the joining newline
379
- }
380
-
381
- appendOwnership.set(path, lineMap)
382
-
383
- const appendContent = appends.map((a) => a.content).join('\n')
384
- existing.content = existing.content + '\n' + appendContent
385
- } else {
386
- files.set(path, {
387
- content: appends.map((a) => a.content).join('\n'),
388
- integrationId: appends[0]?.integrationId ?? 'base',
389
- })
390
- fileOwnership.set(path, appends[0]?.integrationId ?? 'base')
391
- }
392
- }
393
-
394
- // Note: Custom templates don't add files directly - they just specify which integrations to use
395
- // The template's integration list should already be resolved into chosenIntegrations by the caller
396
-
397
- // Build output with attributions
398
- const outputFiles: Record<string, string> = {}
399
- const attributedFiles: AttributedCompileOutput['attributedFiles'] = {}
400
-
401
- for (const [path, { content, integrationId }] of files) {
402
- outputFiles[path] = content
403
-
404
- const lines = content.split('\n')
405
- const attributions: Array<LineAttribution> = []
406
- const appendLineMap = appendOwnership.get(path)
407
- const hookAttrMap = hookAttributions.get(path)
408
-
409
- for (let i = 0; i < lines.length; i++) {
410
- const lineNumber = i + 1
411
-
412
- // Priority: append > hook > file owner
413
- let owningIntegrationId = integrationId
414
- const appendIntegrationId = appendLineMap?.get(lineNumber)
415
- const hookAttr = hookAttrMap?.find(
416
- (a) => a.line === lineNumber,
417
- )
418
-
419
- if (appendIntegrationId) {
420
- owningIntegrationId = appendIntegrationId
421
- } else if (hookAttr) {
422
- owningIntegrationId = hookAttr.integrationId
423
- }
424
-
425
- attributions.push({
426
- lineNumber,
427
- featureId: owningIntegrationId,
428
- featureName: integrationNames.get(owningIntegrationId) || owningIntegrationId,
429
- })
430
- }
431
-
432
- attributedFiles[path] = {
433
- path,
434
- content,
435
- attributions,
436
- }
437
- }
438
-
439
- // Add package.json with line-by-line attribution
440
- outputFiles['package.json'] = buildPackageJson(options, packages)
441
- const pkgJsonLines = outputFiles['package.json'].split('\n')
442
- const pkgJsonAttributions: Array<LineAttribution> = []
443
-
444
- for (let i = 0; i < pkgJsonLines.length; i++) {
445
- const line = pkgJsonLines[i]!
446
- const lineNumber = i + 1
447
- let integrationId = 'base'
448
-
449
- // Check if this line contains a package name we're tracking
450
- // JSON format: "package-name": "version"
451
- const match = line.match(/^\s*"([^"]+)":\s*"[^"]+"/)
452
- if (match) {
453
- const pkgName = match[1]
454
- // Check in order: dependencies, devDependencies, scripts
455
- const depIntegration = packageAttribution.dependencies.get(pkgName!)
456
- const devDepIntegration = packageAttribution.devDependencies.get(pkgName!)
457
- const scriptIntegration = packageAttribution.scripts.get(pkgName!)
458
- if (depIntegration) {
459
- integrationId = depIntegration
460
- } else if (devDepIntegration) {
461
- integrationId = devDepIntegration
462
- } else if (scriptIntegration) {
463
- integrationId = scriptIntegration
464
- }
465
- }
466
-
467
- pkgJsonAttributions.push({
468
- lineNumber,
469
- featureId: integrationId,
470
- featureName: integrationNames.get(integrationId) || integrationId,
471
- })
472
- }
473
-
474
- attributedFiles['package.json'] = {
475
- path: 'package.json',
476
- content: outputFiles['package.json'],
477
- attributions: pkgJsonAttributions,
478
- }
479
-
480
- // Deduplicate env vars (keep integration attribution)
481
- const seenEnvVars = new Set<string>()
482
- const uniqueEnvVars = envVars.filter((v) => {
483
- if (seenEnvVars.has(v.name)) return false
484
- seenEnvVars.add(v.name)
485
- return true
486
- })
487
-
488
- // Generate .env.example with attribution
489
- if (uniqueEnvVars.length > 0) {
490
- const envLines: Array<{ text: string; integrationId: string }> = []
491
- envLines.push({
492
- text: '# Environment Variables',
493
- integrationId: 'base',
494
- })
495
- envLines.push({
496
- text: '# Copy this file to .env.local and fill in your values',
497
- integrationId: 'base',
498
- })
499
- envLines.push({ text: '', integrationId: 'base' })
500
-
501
- // Group by integration
502
- const envByIntegration = new Map<string, Array<(typeof uniqueEnvVars)[0]>>()
503
- for (const envVar of uniqueEnvVars) {
504
- const id = envVar.integrationId
505
- if (!envByIntegration.has(id)) {
506
- envByIntegration.set(id, [])
507
- }
508
- envByIntegration.get(id)!.push(envVar)
509
- }
510
-
511
- for (const [integrationId, vars] of envByIntegration) {
512
- const integrationName = integrationNames.get(integrationId) || integrationId
513
- envLines.push({ text: `# ${integrationName}`, integrationId })
514
- for (const v of vars) {
515
- envLines.push({
516
- text: `# ${v.description}${v.required ? ' (required)' : ''}`,
517
- integrationId,
518
- })
519
- envLines.push({
520
- text: `${v.name}=${v.example || ''}`,
521
- integrationId,
522
- })
523
- }
524
- envLines.push({ text: '', integrationId: 'base' })
525
- }
526
-
527
- const envContent = envLines.map((l) => l.text).join('\n')
528
- outputFiles['.env.example'] = envContent
529
-
530
- attributedFiles['.env.example'] = {
531
- path: '.env.example',
532
- content: envContent,
533
- attributions: envLines.map((l, i) => ({
534
- lineNumber: i + 1,
535
- featureId: l.integrationId,
536
- featureName: integrationNames.get(l.integrationId) || l.integrationId,
537
- })),
538
- }
539
- }
540
-
541
- // Strip integrationId from envVars for output
542
- const outputEnvVars = uniqueEnvVars.map(({ integrationId, ...rest }) => rest)
543
-
544
- return {
545
- files: outputFiles,
546
- packages,
547
- envVars: outputEnvVars,
548
- warnings,
549
- attributedFiles,
550
- }
551
- }
@@ -1,118 +0,0 @@
1
- import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
2
- import { resolve } from 'node:path'
3
- import { afterEach, beforeEach, describe, expect, it } from 'vitest'
4
- import { CONFIG_FILE, readConfigFile, writeConfigFile } from './config-file.js'
5
- import type { CompileOptions } from './types.js'
6
-
7
- const TEST_DIR = resolve(__dirname, '__test_config_file__')
8
-
9
- const baseOptions: CompileOptions = {
10
- projectName: 'test-project',
11
- framework: 'react',
12
- mode: 'file-router',
13
- typescript: true,
14
- tailwind: true,
15
- packageManager: 'pnpm',
16
- chosenIntegrations: [
17
- {
18
- id: 'tanstack-query',
19
- name: 'TanStack Query',
20
- description: 'Data fetching',
21
- type: 'integration',
22
- phase: 'integration',
23
- modes: ['file-router'],
24
- files: {},
25
- deletedFiles: [],
26
- },
27
- ],
28
- integrationOptions: {},
29
- }
30
-
31
- describe('config-file', () => {
32
- beforeEach(() => {
33
- mkdirSync(TEST_DIR, { recursive: true })
34
- })
35
-
36
- afterEach(() => {
37
- rmSync(TEST_DIR, { recursive: true, force: true })
38
- })
39
-
40
- describe('writeConfigFile', () => {
41
- it('should write config file with correct structure', async () => {
42
- await writeConfigFile(TEST_DIR, baseOptions)
43
-
44
- const config = await readConfigFile(TEST_DIR)
45
- expect(config).not.toBeNull()
46
- expect(config!.version).toBe(1)
47
- expect(config!.projectName).toBe('test-project')
48
- expect(config!.framework).toBe('react')
49
- expect(config!.mode).toBe('file-router')
50
- expect(config!.typescript).toBe(true)
51
- expect(config!.tailwind).toBe(true)
52
- expect(config!.packageManager).toBe('pnpm')
53
- })
54
-
55
- it('should persist integration IDs', async () => {
56
- await writeConfigFile(TEST_DIR, baseOptions)
57
-
58
- const config = await readConfigFile(TEST_DIR)
59
- expect(config!.chosenIntegrations).toEqual(['tanstack-query'])
60
- })
61
-
62
- it('should persist custom template ID if provided', async () => {
63
- await writeConfigFile(TEST_DIR, {
64
- ...baseOptions,
65
- customTemplate: {
66
- id: 'my-template',
67
- name: 'My Template',
68
- description: 'Test template',
69
- framework: 'react',
70
- mode: 'file-router',
71
- typescript: true,
72
- tailwind: true,
73
- integrations: [],
74
- },
75
- })
76
-
77
- const config = await readConfigFile(TEST_DIR)
78
- expect(config!.customTemplate).toBe('my-template')
79
- })
80
- })
81
-
82
- describe('readConfigFile', () => {
83
- it('should return null if config file does not exist', async () => {
84
- const config = await readConfigFile(TEST_DIR)
85
- expect(config).toBeNull()
86
- })
87
-
88
- it('should return null for invalid JSON', async () => {
89
- writeFileSync(resolve(TEST_DIR, CONFIG_FILE), 'not valid json')
90
-
91
- const config = await readConfigFile(TEST_DIR)
92
- expect(config).toBeNull()
93
- })
94
-
95
- it('should read valid config file', async () => {
96
- writeFileSync(
97
- resolve(TEST_DIR, CONFIG_FILE),
98
- JSON.stringify({
99
- version: 1,
100
- projectName: 'existing-project',
101
- framework: 'react',
102
- mode: 'file-router',
103
- typescript: true,
104
- tailwind: false,
105
- packageManager: 'npm',
106
- chosenIntegrations: ['clerk'],
107
- }),
108
- )
109
-
110
- const config = await readConfigFile(TEST_DIR)
111
- expect(config).not.toBeNull()
112
- expect(config!.projectName).toBe('existing-project')
113
- expect(config!.tailwind).toBe(false)
114
- expect(config!.packageManager).toBe('npm')
115
- expect(config!.chosenIntegrations).toEqual(['clerk'])
116
- })
117
- })
118
- })