@tanstack/cli 0.61.1 → 0.62.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/package.json +5 -1
- package/skills/CHANGELOG.md +18 -0
- package/skills/add-addons-existing-app/SKILL.md +113 -0
- package/skills/choose-ecosystem-integrations/SKILL.md +140 -0
- package/skills/choose-ecosystem-integrations/references/authentication-providers.md +19 -0
- package/skills/choose-ecosystem-integrations/references/data-layer-providers.md +20 -0
- package/skills/choose-ecosystem-integrations/references/deployment-targets.md +19 -0
- package/skills/create-app-scaffold/SKILL.md +132 -0
- package/skills/create-app-scaffold/references/create-flag-compatibility-matrix.md +34 -0
- package/skills/create-app-scaffold/references/deployment-providers.md +19 -0
- package/skills/create-app-scaffold/references/framework-adapters.md +17 -0
- package/skills/create-app-scaffold/references/toolchains.md +17 -0
- package/skills/maintain-custom-addons-dev-watch/SKILL.md +118 -0
- package/skills/query-docs-library-metadata/SKILL.md +85 -0
- package/skills/query-docs-library-metadata/references/discovery-command-output-schemas.md +70 -0
- package/CHANGELOG.md +0 -815
- package/playwright-report/index.html +0 -85
- package/playwright.config.ts +0 -21
- package/src/bin.ts +0 -15
- package/src/cli.ts +0 -1099
- package/src/command-line.ts +0 -612
- package/src/dev-watch.ts +0 -564
- package/src/discovery.ts +0 -209
- package/src/file-syncer.ts +0 -263
- package/src/index.ts +0 -21
- package/src/options.ts +0 -280
- package/src/types.ts +0 -27
- package/src/ui-environment.ts +0 -74
- package/src/ui-prompts.ts +0 -387
- package/src/utils.ts +0 -30
- package/test-results/.last-run.json +0 -4
- package/tests/command-line.test.ts +0 -703
- package/tests/index.test.ts +0 -9
- package/tests/options.test.ts +0 -281
- package/tests/setupVitest.ts +0 -6
- package/tests/ui-environment.test.ts +0 -97
- package/tests/ui-prompts.test.ts +0 -233
- package/tests-e2e/addons-smoke.spec.ts +0 -31
- package/tests-e2e/create-smoke.spec.ts +0 -39
- package/tests-e2e/helpers.ts +0 -526
- package/tests-e2e/matrix-opportunistic.spec.ts +0 -142
- package/tests-e2e/router-only-smoke.spec.ts +0 -54
- package/tests-e2e/solid-smoke.spec.ts +0 -26
- package/tests-e2e/templates-smoke.spec.ts +0 -52
- package/tsconfig.json +0 -17
- package/vitest.config.js +0 -8
package/src/command-line.ts
DELETED
|
@@ -1,612 +0,0 @@
|
|
|
1
|
-
import { resolve } from 'node:path'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
DEFAULT_PACKAGE_MANAGER,
|
|
6
|
-
finalizeAddOns,
|
|
7
|
-
getFrameworkById,
|
|
8
|
-
getPackageManager,
|
|
9
|
-
getRawRegistry,
|
|
10
|
-
loadStarter,
|
|
11
|
-
populateAddOnOptionsDefaults,
|
|
12
|
-
} from '@tanstack/create'
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
getCurrentDirectoryName,
|
|
16
|
-
sanitizePackageName,
|
|
17
|
-
validateProjectName,
|
|
18
|
-
} from './utils.js'
|
|
19
|
-
import type { Options } from '@tanstack/create'
|
|
20
|
-
|
|
21
|
-
import type { CliOptions } from './types.js'
|
|
22
|
-
|
|
23
|
-
const SUPPORTED_LEGACY_TEMPLATES = new Set([
|
|
24
|
-
'file-router',
|
|
25
|
-
'typescript',
|
|
26
|
-
'tsx',
|
|
27
|
-
])
|
|
28
|
-
|
|
29
|
-
const LEGACY_TEMPLATE_ALIASES = new Set(['javascript', 'js', 'jsx'])
|
|
30
|
-
|
|
31
|
-
function getLegacyTemplateValue(templateValue?: string) {
|
|
32
|
-
if (!templateValue) {
|
|
33
|
-
return undefined
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const normalized = templateValue.toLowerCase().trim()
|
|
37
|
-
if (
|
|
38
|
-
SUPPORTED_LEGACY_TEMPLATES.has(normalized) ||
|
|
39
|
-
LEGACY_TEMPLATE_ALIASES.has(normalized)
|
|
40
|
-
) {
|
|
41
|
-
return normalized
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return undefined
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function slugifyStarterName(value: string) {
|
|
48
|
-
return value
|
|
49
|
-
.trim()
|
|
50
|
-
.toLowerCase()
|
|
51
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
52
|
-
.replace(/^-+|-+$/g, '')
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function humanizeStarterId(value: string) {
|
|
56
|
-
return value
|
|
57
|
-
.split(/[-_]/g)
|
|
58
|
-
.filter(Boolean)
|
|
59
|
-
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
60
|
-
.join(' ')
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function isLikelyStarterUrlOrPath(value: string) {
|
|
64
|
-
return (
|
|
65
|
-
/^https?:\/\//i.test(value) ||
|
|
66
|
-
/^file:\/\//i.test(value) ||
|
|
67
|
-
value.startsWith('./') ||
|
|
68
|
-
value.startsWith('../') ||
|
|
69
|
-
value.startsWith('/') ||
|
|
70
|
-
/^[a-zA-Z]:[\\/]/.test(value)
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function getStarterIdsFromUrl(starterUrl: string) {
|
|
75
|
-
const ids = new Set<string>()
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const pathname = new URL(starterUrl).pathname
|
|
79
|
-
const parts = pathname.split('/').filter(Boolean)
|
|
80
|
-
const lastPart = parts.at(-1)?.toLowerCase()
|
|
81
|
-
|
|
82
|
-
if (lastPart) {
|
|
83
|
-
ids.add(lastPart.replace(/\.json$/i, ''))
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
(lastPart === 'starter.json' || lastPart === 'template.json') &&
|
|
88
|
-
parts.length >= 2
|
|
89
|
-
) {
|
|
90
|
-
ids.add(parts.at(-2)!.toLowerCase())
|
|
91
|
-
}
|
|
92
|
-
} catch {
|
|
93
|
-
// Ignore URL parse errors and rely on other id heuristics.
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return ids
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function resolveMonorepoStarterById(
|
|
100
|
-
starterId: string,
|
|
101
|
-
preferredFramework?: string,
|
|
102
|
-
) {
|
|
103
|
-
const normalized = starterId.toLowerCase().trim()
|
|
104
|
-
const idVariants = Array.from(
|
|
105
|
-
new Set([
|
|
106
|
-
normalized,
|
|
107
|
-
normalized.replace(/-template$/i, ''),
|
|
108
|
-
normalized.replace(/-starter$/i, ''),
|
|
109
|
-
]),
|
|
110
|
-
).filter(Boolean)
|
|
111
|
-
|
|
112
|
-
const cwd = process.cwd()
|
|
113
|
-
const rootCandidates = [
|
|
114
|
-
cwd,
|
|
115
|
-
resolve(cwd, '..'),
|
|
116
|
-
resolve(cwd, '../..'),
|
|
117
|
-
resolve(cwd, '../../..'),
|
|
118
|
-
]
|
|
119
|
-
|
|
120
|
-
const frameworkOrder = preferredFramework
|
|
121
|
-
? [preferredFramework, ...['react', 'solid'].filter((f) => f !== preferredFramework)]
|
|
122
|
-
: ['react', 'solid']
|
|
123
|
-
|
|
124
|
-
for (const root of rootCandidates) {
|
|
125
|
-
for (const framework of frameworkOrder) {
|
|
126
|
-
for (const id of idVariants) {
|
|
127
|
-
const templatePath = resolve(root, 'examples', framework, id, 'template.json')
|
|
128
|
-
if (fs.existsSync(templatePath)) {
|
|
129
|
-
return templatePath
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const starterPath = resolve(root, 'examples', framework, id, 'starter.json')
|
|
133
|
-
if (fs.existsSync(starterPath)) {
|
|
134
|
-
return starterPath
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return undefined
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export async function resolveStarterSpecifier(
|
|
144
|
-
starterSpecifier: string,
|
|
145
|
-
preferredFramework?: string,
|
|
146
|
-
) {
|
|
147
|
-
const normalized = starterSpecifier.trim()
|
|
148
|
-
|
|
149
|
-
if (!normalized || isLikelyStarterUrlOrPath(normalized)) {
|
|
150
|
-
return normalized
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const registry = await getRawRegistry()
|
|
154
|
-
if (registry && registry.starters?.length) {
|
|
155
|
-
const lookup = normalized.toLowerCase()
|
|
156
|
-
const matches = registry.starters.filter((starter) => {
|
|
157
|
-
const candidateIds = new Set<string>()
|
|
158
|
-
candidateIds.add(starter.name.toLowerCase())
|
|
159
|
-
candidateIds.add(slugifyStarterName(starter.name))
|
|
160
|
-
|
|
161
|
-
for (const id of getStarterIdsFromUrl(starter.url)) {
|
|
162
|
-
candidateIds.add(id)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return candidateIds.has(lookup)
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
const frameworkMatch = preferredFramework
|
|
169
|
-
? matches.find(
|
|
170
|
-
(starter) => starter.framework.toLowerCase() === preferredFramework,
|
|
171
|
-
)
|
|
172
|
-
: undefined
|
|
173
|
-
|
|
174
|
-
if (frameworkMatch) {
|
|
175
|
-
return frameworkMatch.url
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (matches.length > 0) {
|
|
179
|
-
return matches[0].url
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const monorepoStarterPath = resolveMonorepoStarterById(
|
|
184
|
-
normalized,
|
|
185
|
-
preferredFramework,
|
|
186
|
-
)
|
|
187
|
-
if (monorepoStarterPath) {
|
|
188
|
-
return monorepoStarterPath
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (!registry || !registry.starters?.length) {
|
|
192
|
-
throw new Error(
|
|
193
|
-
`Could not resolve template id "${normalized}" because no template registry is configured. Pass a template URL (or set CTA_REGISTRY).`,
|
|
194
|
-
)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const availableIds = Array.from(
|
|
198
|
-
new Set(
|
|
199
|
-
registry.starters.flatMap((starter) => {
|
|
200
|
-
const ids = [slugifyStarterName(starter.name)]
|
|
201
|
-
ids.push(...Array.from(getStarterIdsFromUrl(starter.url)))
|
|
202
|
-
return ids
|
|
203
|
-
}),
|
|
204
|
-
),
|
|
205
|
-
)
|
|
206
|
-
.filter(Boolean)
|
|
207
|
-
.sort()
|
|
208
|
-
|
|
209
|
-
throw new Error(
|
|
210
|
-
`Unknown template id "${normalized}". Available built-in templates: ${availableIds.join(', ')}`,
|
|
211
|
-
)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export async function listTemplateChoices(preferredFramework?: string): Promise<
|
|
215
|
-
Array<{
|
|
216
|
-
id: string
|
|
217
|
-
name: string
|
|
218
|
-
description?: string
|
|
219
|
-
framework: string
|
|
220
|
-
}>
|
|
221
|
-
> {
|
|
222
|
-
const frameworkFilter = preferredFramework?.toLowerCase()
|
|
223
|
-
const deduped = new Map<
|
|
224
|
-
string,
|
|
225
|
-
{ id: string; name: string; description?: string; framework: string }
|
|
226
|
-
>()
|
|
227
|
-
|
|
228
|
-
const registry = await getRawRegistry()
|
|
229
|
-
for (const starter of registry?.starters || []) {
|
|
230
|
-
const framework = starter.framework.toLowerCase()
|
|
231
|
-
if (frameworkFilter && framework !== frameworkFilter) {
|
|
232
|
-
continue
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const ids = Array.from(getStarterIdsFromUrl(starter.url))
|
|
236
|
-
const id = ids[0] || slugifyStarterName(starter.name)
|
|
237
|
-
if (!id) {
|
|
238
|
-
continue
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const key = `${framework}:${id}`
|
|
242
|
-
if (!deduped.has(key)) {
|
|
243
|
-
deduped.set(key, {
|
|
244
|
-
id,
|
|
245
|
-
name: starter.name,
|
|
246
|
-
description: starter.description,
|
|
247
|
-
framework,
|
|
248
|
-
})
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const cwd = process.cwd()
|
|
253
|
-
const rootCandidates = [
|
|
254
|
-
cwd,
|
|
255
|
-
resolve(cwd, '..'),
|
|
256
|
-
resolve(cwd, '../..'),
|
|
257
|
-
resolve(cwd, '../../..'),
|
|
258
|
-
]
|
|
259
|
-
|
|
260
|
-
const frameworks = frameworkFilter ? [frameworkFilter] : ['react', 'solid']
|
|
261
|
-
|
|
262
|
-
for (const root of rootCandidates) {
|
|
263
|
-
for (const framework of frameworks) {
|
|
264
|
-
const frameworkDir = resolve(root, 'examples', framework)
|
|
265
|
-
if (!fs.existsSync(frameworkDir) || !fs.statSync(frameworkDir).isDirectory()) {
|
|
266
|
-
continue
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
for (const entry of fs.readdirSync(frameworkDir, { withFileTypes: true })) {
|
|
270
|
-
if (!entry.isDirectory()) {
|
|
271
|
-
continue
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const id = entry.name
|
|
275
|
-
const key = `${framework}:${id}`
|
|
276
|
-
if (deduped.has(key)) {
|
|
277
|
-
continue
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const templatePath = resolve(frameworkDir, id, 'template.json')
|
|
281
|
-
const starterPath = resolve(frameworkDir, id, 'starter.json')
|
|
282
|
-
if (!fs.existsSync(templatePath) && !fs.existsSync(starterPath)) {
|
|
283
|
-
continue
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
let name = humanizeStarterId(id)
|
|
287
|
-
let description: string | undefined
|
|
288
|
-
|
|
289
|
-
const templateInfoPath = resolve(frameworkDir, id, 'template-info.json')
|
|
290
|
-
if (fs.existsSync(templateInfoPath)) {
|
|
291
|
-
try {
|
|
292
|
-
const info = JSON.parse(fs.readFileSync(templateInfoPath, 'utf8')) as {
|
|
293
|
-
name?: string
|
|
294
|
-
description?: string
|
|
295
|
-
}
|
|
296
|
-
if (info.name) {
|
|
297
|
-
name = info.name
|
|
298
|
-
}
|
|
299
|
-
description = info.description
|
|
300
|
-
} catch {
|
|
301
|
-
// Ignore malformed template-info files and use fallback values.
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
deduped.set(key, {
|
|
306
|
-
id,
|
|
307
|
-
name,
|
|
308
|
-
description,
|
|
309
|
-
framework,
|
|
310
|
-
})
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return Array.from(deduped.values()).sort((a, b) => a.name.localeCompare(b.name))
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
export function validateLegacyCreateFlags(cliOptions: CliOptions): {
|
|
319
|
-
warnings: Array<string>
|
|
320
|
-
error?: string
|
|
321
|
-
} {
|
|
322
|
-
const warnings: Array<string> = []
|
|
323
|
-
const legacyTemplate = getLegacyTemplateValue(cliOptions.template)
|
|
324
|
-
|
|
325
|
-
if (cliOptions.starter) {
|
|
326
|
-
warnings.push(
|
|
327
|
-
'The --starter flag is deprecated; prefer --template instead. Backward compatibility remains for now.',
|
|
328
|
-
)
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (cliOptions.starter && cliOptions.template && !legacyTemplate) {
|
|
332
|
-
warnings.push(
|
|
333
|
-
'Both --starter and --template were provided. --template takes precedence.',
|
|
334
|
-
)
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (cliOptions.routerOnly) {
|
|
338
|
-
warnings.push(
|
|
339
|
-
'The --router-only flag enables router-only compatibility mode. Start-dependent add-ons, deployment adapters, and templates are disabled; only the base template and optional toolchain are supported.',
|
|
340
|
-
)
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (cliOptions.routerOnly && cliOptions.addOns) {
|
|
344
|
-
warnings.push(
|
|
345
|
-
'Ignoring --add-ons in router-only compatibility mode.',
|
|
346
|
-
)
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (cliOptions.routerOnly && cliOptions.deployment) {
|
|
350
|
-
warnings.push(
|
|
351
|
-
'Ignoring --deployment in router-only compatibility mode.',
|
|
352
|
-
)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (cliOptions.routerOnly && cliOptions.starter) {
|
|
356
|
-
warnings.push('Ignoring --starter/--template in router-only compatibility mode.')
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (cliOptions.routerOnly && cliOptions.templateId) {
|
|
360
|
-
warnings.push('Ignoring --template-id in router-only compatibility mode.')
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (cliOptions.tailwind === true) {
|
|
364
|
-
warnings.push(
|
|
365
|
-
'The --tailwind flag is deprecated and ignored. Tailwind is always enabled in TanStack Start scaffolds.',
|
|
366
|
-
)
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (cliOptions.tailwind === false) {
|
|
370
|
-
warnings.push(
|
|
371
|
-
'The --no-tailwind flag is deprecated and ignored. Tailwind opt-out is intentionally unsupported to keep add-on permutations maintainable; remove Tailwind after scaffolding if needed.',
|
|
372
|
-
)
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (!legacyTemplate) {
|
|
376
|
-
return { warnings }
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const template = legacyTemplate
|
|
380
|
-
|
|
381
|
-
if (template === 'javascript' || template === 'js' || template === 'jsx') {
|
|
382
|
-
return {
|
|
383
|
-
warnings,
|
|
384
|
-
error:
|
|
385
|
-
'JavaScript/JSX templates are not supported. TanStack Start file-router templates are TypeScript-only.',
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (!SUPPORTED_LEGACY_TEMPLATES.has(template)) {
|
|
390
|
-
return {
|
|
391
|
-
warnings,
|
|
392
|
-
error: `Invalid --template value: ${cliOptions.template}. Supported values are: file-router, typescript, tsx.`,
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
warnings.push('The --template flag is deprecated and mapped for compatibility.')
|
|
397
|
-
|
|
398
|
-
return { warnings }
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
export async function normalizeOptions(
|
|
402
|
-
cliOptions: CliOptions,
|
|
403
|
-
forcedAddOns?: Array<string>,
|
|
404
|
-
opts?: {
|
|
405
|
-
disableNameCheck?: boolean
|
|
406
|
-
forcedDeployment?: string
|
|
407
|
-
},
|
|
408
|
-
): Promise<Options | undefined> {
|
|
409
|
-
let projectName = (cliOptions.projectName ?? '').trim()
|
|
410
|
-
let targetDir: string
|
|
411
|
-
|
|
412
|
-
// Handle "." as project name - use current directory
|
|
413
|
-
if (projectName === '.') {
|
|
414
|
-
projectName = sanitizePackageName(getCurrentDirectoryName())
|
|
415
|
-
targetDir = resolve(process.cwd())
|
|
416
|
-
} else {
|
|
417
|
-
targetDir = resolve(process.cwd(), projectName)
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (!projectName && !opts?.disableNameCheck) {
|
|
421
|
-
return undefined
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (projectName) {
|
|
425
|
-
const { valid, error } = validateProjectName(projectName)
|
|
426
|
-
if (!valid) {
|
|
427
|
-
console.error(error)
|
|
428
|
-
process.exit(1)
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Mode is always file-router (TanStack Start)
|
|
433
|
-
let mode = 'file-router'
|
|
434
|
-
let routerOnly = !!cliOptions.routerOnly
|
|
435
|
-
|
|
436
|
-
const legacyTemplate = getLegacyTemplateValue(cliOptions.template)
|
|
437
|
-
|
|
438
|
-
if (!cliOptions.starter) {
|
|
439
|
-
if (cliOptions.template && !legacyTemplate) {
|
|
440
|
-
cliOptions.starter = cliOptions.template
|
|
441
|
-
} else if (cliOptions.templateId) {
|
|
442
|
-
cliOptions.starter = cliOptions.templateId
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const template = legacyTemplate
|
|
447
|
-
if (template && template !== 'file-router') {
|
|
448
|
-
routerOnly = true
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
if (!cliOptions.starter && cliOptions.templateId) {
|
|
452
|
-
cliOptions.starter = cliOptions.templateId
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const preferredFramework = (cliOptions.framework || 'react').toLowerCase()
|
|
456
|
-
|
|
457
|
-
const starter = !routerOnly && cliOptions.starter
|
|
458
|
-
? await loadStarter(
|
|
459
|
-
await resolveStarterSpecifier(cliOptions.starter, preferredFramework),
|
|
460
|
-
)
|
|
461
|
-
: undefined
|
|
462
|
-
|
|
463
|
-
// TypeScript and Tailwind are always enabled with TanStack Start
|
|
464
|
-
const typescript = true
|
|
465
|
-
const tailwind = true
|
|
466
|
-
|
|
467
|
-
if (starter) {
|
|
468
|
-
cliOptions.framework = starter.framework
|
|
469
|
-
mode = starter.mode
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const framework = getFrameworkById(cliOptions.framework || 'react')!
|
|
473
|
-
|
|
474
|
-
async function selectAddOns() {
|
|
475
|
-
// Edge case for Windows Powershell
|
|
476
|
-
if (Array.isArray(cliOptions.addOns) && cliOptions.addOns.length === 1) {
|
|
477
|
-
const parseSeparatedArgs = cliOptions.addOns[0].split(' ')
|
|
478
|
-
if (parseSeparatedArgs.length > 1) {
|
|
479
|
-
cliOptions.addOns = parseSeparatedArgs
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (
|
|
484
|
-
Array.isArray(cliOptions.addOns) ||
|
|
485
|
-
starter?.dependsOn ||
|
|
486
|
-
forcedAddOns ||
|
|
487
|
-
cliOptions.toolchain ||
|
|
488
|
-
cliOptions.deployment
|
|
489
|
-
) {
|
|
490
|
-
const selectedAddOns = new Set<string>([
|
|
491
|
-
...(routerOnly ? [] : (starter?.dependsOn || [])),
|
|
492
|
-
...(routerOnly ? [] : (forcedAddOns || [])),
|
|
493
|
-
])
|
|
494
|
-
if (!routerOnly && cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
|
|
495
|
-
for (const a of cliOptions.addOns) {
|
|
496
|
-
if (a.toLowerCase() === 'start') {
|
|
497
|
-
continue
|
|
498
|
-
}
|
|
499
|
-
selectedAddOns.add(a)
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
if (cliOptions.toolchain) {
|
|
503
|
-
selectedAddOns.add(cliOptions.toolchain)
|
|
504
|
-
}
|
|
505
|
-
if (!routerOnly && cliOptions.deployment) {
|
|
506
|
-
selectedAddOns.add(cliOptions.deployment)
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
if (!routerOnly && !cliOptions.deployment && opts?.forcedDeployment) {
|
|
510
|
-
selectedAddOns.add(opts.forcedDeployment)
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
return await finalizeAddOns(framework, mode, Array.from(selectedAddOns))
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
return []
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const includeExamples = cliOptions.examples ?? !routerOnly
|
|
520
|
-
const chosenAddOnsRaw = await selectAddOns()
|
|
521
|
-
const chosenAddOns = includeExamples
|
|
522
|
-
? chosenAddOnsRaw
|
|
523
|
-
: chosenAddOnsRaw.filter((addOn) => addOn.type !== 'example')
|
|
524
|
-
|
|
525
|
-
// Handle add-on configuration option
|
|
526
|
-
let addOnOptionsFromCLI = {}
|
|
527
|
-
if (cliOptions.addOnConfig) {
|
|
528
|
-
try {
|
|
529
|
-
addOnOptionsFromCLI = JSON.parse(cliOptions.addOnConfig)
|
|
530
|
-
} catch (error) {
|
|
531
|
-
console.error('Error parsing add-on config:', error)
|
|
532
|
-
process.exit(1)
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const normalized = {
|
|
537
|
-
projectName: projectName,
|
|
538
|
-
targetDir,
|
|
539
|
-
framework,
|
|
540
|
-
mode,
|
|
541
|
-
typescript,
|
|
542
|
-
tailwind,
|
|
543
|
-
packageManager:
|
|
544
|
-
cliOptions.packageManager ||
|
|
545
|
-
getPackageManager() ||
|
|
546
|
-
DEFAULT_PACKAGE_MANAGER,
|
|
547
|
-
git: cliOptions.git ?? true,
|
|
548
|
-
install: cliOptions.install,
|
|
549
|
-
chosenAddOns,
|
|
550
|
-
addOnOptions: {
|
|
551
|
-
...populateAddOnOptionsDefaults(chosenAddOns),
|
|
552
|
-
...addOnOptionsFromCLI,
|
|
553
|
-
},
|
|
554
|
-
starter: starter,
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
;(normalized as Options & { includeExamples?: boolean }).includeExamples =
|
|
558
|
-
includeExamples
|
|
559
|
-
;(normalized as Options & { envVarValues?: Record<string, string> }).envVarValues =
|
|
560
|
-
{}
|
|
561
|
-
|
|
562
|
-
return normalized
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
export function validateDevWatchOptions(cliOptions: CliOptions): {
|
|
566
|
-
valid: boolean
|
|
567
|
-
error?: string
|
|
568
|
-
} {
|
|
569
|
-
if (!cliOptions.devWatch) {
|
|
570
|
-
return { valid: true }
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Validate watch path exists
|
|
574
|
-
const watchPath = resolve(process.cwd(), cliOptions.devWatch)
|
|
575
|
-
if (!fs.existsSync(watchPath)) {
|
|
576
|
-
return {
|
|
577
|
-
valid: false,
|
|
578
|
-
error: `Watch path does not exist: ${watchPath}`,
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// Validate it's a directory
|
|
583
|
-
const stats = fs.statSync(watchPath)
|
|
584
|
-
if (!stats.isDirectory()) {
|
|
585
|
-
return {
|
|
586
|
-
valid: false,
|
|
587
|
-
error: `Watch path is not a directory: ${watchPath}`,
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Ensure target directory is specified
|
|
592
|
-
if (!cliOptions.projectName && !cliOptions.targetDir) {
|
|
593
|
-
return {
|
|
594
|
-
valid: false,
|
|
595
|
-
error: 'Project name or target directory is required for dev watch mode',
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Check for framework structure
|
|
600
|
-
const hasAddOns = fs.existsSync(resolve(watchPath, 'add-ons'))
|
|
601
|
-
const hasAssets = fs.existsSync(resolve(watchPath, 'assets'))
|
|
602
|
-
const hasFrameworkJson = fs.existsSync(resolve(watchPath, 'framework.json'))
|
|
603
|
-
|
|
604
|
-
if (!hasAddOns && !hasAssets && !hasFrameworkJson) {
|
|
605
|
-
return {
|
|
606
|
-
valid: false,
|
|
607
|
-
error: `Watch path does not appear to be a valid framework directory: ${watchPath}`,
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
return { valid: true }
|
|
612
|
-
}
|