@tanstack/cli 0.0.8 → 0.48.3

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