@tanstack/cli 0.0.8 → 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.
- package/dist/bin.js +7 -0
- package/dist/cli.js +481 -0
- package/dist/command-line.js +174 -0
- package/dist/dev-watch.js +290 -0
- package/dist/file-syncer.js +148 -0
- package/dist/index.js +1 -0
- package/dist/mcp/api.js +31 -0
- package/dist/mcp/tools.js +250 -0
- package/dist/mcp/types.js +37 -0
- package/dist/mcp.js +121 -0
- package/dist/options.js +162 -0
- package/dist/types/bin.d.ts +2 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/command-line.d.ts +10 -0
- package/dist/types/dev-watch.d.ts +27 -0
- package/dist/types/file-syncer.d.ts +18 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mcp/api.d.ts +4 -0
- package/dist/types/mcp/tools.d.ts +2 -0
- package/dist/types/mcp/types.d.ts +217 -0
- package/dist/types/mcp.d.ts +6 -0
- package/dist/types/options.d.ts +8 -0
- package/dist/types/types.d.ts +25 -0
- package/dist/types/ui-environment.d.ts +2 -0
- package/dist/types/ui-prompts.d.ts +12 -0
- package/dist/types/utils.d.ts +8 -0
- package/dist/types.js +1 -0
- package/dist/ui-environment.js +52 -0
- package/dist/ui-prompts.js +244 -0
- package/dist/utils.js +30 -0
- package/package.json +46 -46
- package/src/bin.ts +6 -93
- package/src/cli.ts +692 -0
- package/src/command-line.ts +236 -0
- package/src/dev-watch.ts +430 -0
- package/src/file-syncer.ts +205 -0
- package/src/index.ts +1 -85
- package/src/mcp.ts +190 -0
- package/src/options.ts +260 -0
- package/src/types.ts +27 -0
- package/src/ui-environment.ts +74 -0
- package/src/ui-prompts.ts +322 -0
- package/src/utils.ts +38 -0
- package/tests/command-line.test.ts +304 -0
- package/tests/index.test.ts +9 -0
- package/tests/mcp.test.ts +225 -0
- package/tests/options.test.ts +304 -0
- package/tests/setupVitest.ts +6 -0
- package/tests/ui-environment.test.ts +97 -0
- package/tests/ui-prompts.test.ts +238 -0
- package/tsconfig.json +17 -0
- package/vitest.config.js +7 -0
- package/dist/bin.cjs +0 -769
- package/dist/bin.d.cts +0 -1
- package/dist/bin.d.mts +0 -1
- package/dist/bin.mjs +0 -768
- package/dist/fetch-CbFFGJEw.cjs +0 -3
- package/dist/fetch-DG5dLrsb.cjs +0 -522
- package/dist/fetch-DhlVXS6S.mjs +0 -390
- package/dist/fetch-I_OVg8JX.mjs +0 -3
- package/dist/index.cjs +0 -37
- package/dist/index.d.cts +0 -1172
- package/dist/index.d.mts +0 -1172
- package/dist/index.mjs +0 -4
- package/dist/template-Szi7-AZJ.mjs +0 -2202
- package/dist/template-lWrIZhCQ.cjs +0 -2314
- package/src/api/fetch.test.ts +0 -114
- package/src/api/fetch.ts +0 -278
- package/src/cache/index.ts +0 -89
- package/src/commands/create.ts +0 -470
- package/src/commands/mcp.test.ts +0 -152
- package/src/commands/mcp.ts +0 -211
- package/src/engine/compile-with-addons.test.ts +0 -302
- package/src/engine/compile.test.ts +0 -404
- package/src/engine/compile.ts +0 -569
- package/src/engine/config-file.test.ts +0 -118
- package/src/engine/config-file.ts +0 -61
- package/src/engine/custom-addons/integration.ts +0 -323
- package/src/engine/custom-addons/shared.test.ts +0 -98
- package/src/engine/custom-addons/shared.ts +0 -281
- package/src/engine/custom-addons/template.test.ts +0 -288
- package/src/engine/custom-addons/template.ts +0 -124
- package/src/engine/template.test.ts +0 -256
- package/src/engine/template.ts +0 -269
- package/src/engine/types.ts +0 -336
- package/src/parse-gitignore.d.ts +0 -5
- package/src/templates/base.ts +0 -883
package/src/engine/compile.ts
DELETED
|
@@ -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
|
-
})
|