@tanstack/cli 0.60.1 → 0.61.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/cli.js +266 -11
- package/dist/command-line.js +103 -8
- package/dist/discovery.js +144 -0
- package/dist/options.js +35 -2
- package/dist/types/command-line.d.ts +7 -0
- package/dist/types/{mcp/types.d.ts → discovery.d.ts} +23 -75
- package/dist/types/types.d.ts +1 -2
- package/dist/types/ui-prompts.d.ts +5 -0
- package/dist/ui-prompts.js +26 -0
- package/package.json +2 -5
- package/playwright-report/index.html +1 -1
- package/src/cli.ts +345 -13
- package/src/command-line.ts +147 -8
- package/src/discovery.ts +209 -0
- package/src/options.ts +46 -0
- package/src/types.ts +1 -2
- package/src/ui-prompts.ts +32 -0
- package/tests/command-line.test.ts +81 -0
- package/tests/options.test.ts +65 -0
- package/tests/ui-prompts.test.ts +28 -0
- package/tests-e2e/create-smoke.spec.ts +12 -12
- package/tests-e2e/router-only-smoke.spec.ts +11 -25
- package/tests-e2e/solid-smoke.spec.ts +3 -2
- package/dist/mcp/api.js +0 -31
- package/dist/mcp/tools.js +0 -250
- package/dist/mcp/types.js +0 -37
- package/dist/mcp.js +0 -181
- package/dist/types/mcp/api.d.ts +0 -4
- package/dist/types/mcp/tools.d.ts +0 -2
- package/dist/types/mcp.d.ts +0 -5
- package/src/mcp/api.ts +0 -42
- package/src/mcp/tools.ts +0 -323
- package/src/mcp/types.ts +0 -46
- package/src/mcp.ts +0 -263
- package/tests/mcp.test.ts +0 -225
package/src/cli.ts
CHANGED
|
@@ -18,8 +18,13 @@ import {
|
|
|
18
18
|
initAddOn,
|
|
19
19
|
initStarter,
|
|
20
20
|
} from '@tanstack/create'
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
import {
|
|
22
|
+
LIBRARY_GROUPS,
|
|
23
|
+
fetchDocContent,
|
|
24
|
+
fetchLibraries,
|
|
25
|
+
fetchPartners,
|
|
26
|
+
searchTanStackDocs,
|
|
27
|
+
} from './discovery.js'
|
|
23
28
|
|
|
24
29
|
import { promptForAddOns, promptForCreateOptions } from './options.js'
|
|
25
30
|
import {
|
|
@@ -206,6 +211,35 @@ export function cli({
|
|
|
206
211
|
|
|
207
212
|
// Mode is always file-router (TanStack Start)
|
|
208
213
|
const defaultMode = 'file-router'
|
|
214
|
+
const categoryAliases: Record<string, string> = {
|
|
215
|
+
db: 'database',
|
|
216
|
+
postgres: 'database',
|
|
217
|
+
sql: 'database',
|
|
218
|
+
login: 'auth',
|
|
219
|
+
authentication: 'auth',
|
|
220
|
+
hosting: 'deployment',
|
|
221
|
+
deploy: 'deployment',
|
|
222
|
+
serverless: 'deployment',
|
|
223
|
+
errors: 'monitoring',
|
|
224
|
+
logging: 'monitoring',
|
|
225
|
+
content: 'cms',
|
|
226
|
+
'api-keys': 'api',
|
|
227
|
+
grid: 'data-grid',
|
|
228
|
+
review: 'code-review',
|
|
229
|
+
courses: 'learning',
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function printJson(data: unknown) {
|
|
233
|
+
console.log(JSON.stringify(data, null, 2))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function parsePositiveInteger(value: string) {
|
|
237
|
+
const parsed = Number(value)
|
|
238
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
239
|
+
throw new InvalidArgumentError('Value must be a positive integer')
|
|
240
|
+
}
|
|
241
|
+
return parsed
|
|
242
|
+
}
|
|
209
243
|
|
|
210
244
|
program
|
|
211
245
|
.name(name)
|
|
@@ -229,8 +263,29 @@ export function cli({
|
|
|
229
263
|
getFrameworkByName(options.framework || defaultFramework || 'React')!,
|
|
230
264
|
defaultMode,
|
|
231
265
|
)
|
|
266
|
+
const visibleAddOns = addOns.filter((a) => !forcedAddOns.includes(a.id))
|
|
267
|
+
if (options.json) {
|
|
268
|
+
printJson(
|
|
269
|
+
visibleAddOns.map((addOn) => ({
|
|
270
|
+
id: addOn.id,
|
|
271
|
+
name: addOn.name,
|
|
272
|
+
description: addOn.description,
|
|
273
|
+
type: addOn.type,
|
|
274
|
+
category: addOn.category,
|
|
275
|
+
phase: addOn.phase,
|
|
276
|
+
modes: addOn.modes,
|
|
277
|
+
link: addOn.link,
|
|
278
|
+
warning: addOn.warning,
|
|
279
|
+
exclusive: addOn.exclusive,
|
|
280
|
+
dependsOn: addOn.dependsOn,
|
|
281
|
+
options: addOn.options,
|
|
282
|
+
})),
|
|
283
|
+
)
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
232
287
|
let hasConfigurableAddOns = false
|
|
233
|
-
for (const addOn of
|
|
288
|
+
for (const addOn of visibleAddOns) {
|
|
234
289
|
const hasOptions =
|
|
235
290
|
addOn.options && Object.keys(addOn.options).length > 0
|
|
236
291
|
const optionMarker = hasOptions ? '*' : ' '
|
|
@@ -261,6 +316,34 @@ export function cli({
|
|
|
261
316
|
process.exit(1)
|
|
262
317
|
}
|
|
263
318
|
|
|
319
|
+
if (options.json) {
|
|
320
|
+
const files = await addOn.getFiles()
|
|
321
|
+
printJson({
|
|
322
|
+
id: addOn.id,
|
|
323
|
+
name: addOn.name,
|
|
324
|
+
description: addOn.description,
|
|
325
|
+
type: addOn.type,
|
|
326
|
+
category: addOn.category,
|
|
327
|
+
phase: addOn.phase,
|
|
328
|
+
modes: addOn.modes,
|
|
329
|
+
link: addOn.link,
|
|
330
|
+
warning: addOn.warning,
|
|
331
|
+
exclusive: addOn.exclusive,
|
|
332
|
+
dependsOn: addOn.dependsOn,
|
|
333
|
+
options: addOn.options,
|
|
334
|
+
routes: addOn.routes,
|
|
335
|
+
packageAdditions: addOn.packageAdditions,
|
|
336
|
+
shadcnComponents: addOn.shadcnComponents,
|
|
337
|
+
integrations: addOn.integrations,
|
|
338
|
+
readme: addOn.readme,
|
|
339
|
+
files,
|
|
340
|
+
author: addOn.author,
|
|
341
|
+
version: addOn.version,
|
|
342
|
+
license: addOn.license,
|
|
343
|
+
})
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
|
|
264
347
|
console.log(
|
|
265
348
|
`${chalk.bold.cyan('Add-on Details:')} ${chalk.bold(addOn.name)}`,
|
|
266
349
|
)
|
|
@@ -283,7 +366,7 @@ export function cli({
|
|
|
283
366
|
if (addOn.options && Object.keys(addOn.options).length > 0) {
|
|
284
367
|
console.log(`\n${chalk.bold.yellow('Configuration Options:')}`)
|
|
285
368
|
for (const [optionName, option] of Object.entries(addOn.options)) {
|
|
286
|
-
if (
|
|
369
|
+
if ('type' in option) {
|
|
287
370
|
const opt = option as any
|
|
288
371
|
console.log(` ${chalk.bold(optionName)}:`)
|
|
289
372
|
console.log(` Label: ${opt.label}`)
|
|
@@ -525,6 +608,7 @@ export function cli({
|
|
|
525
608
|
'--addon-details <addon-id>',
|
|
526
609
|
'show detailed information about a specific add-on',
|
|
527
610
|
)
|
|
611
|
+
.option('--json', 'output JSON for automation', false)
|
|
528
612
|
.option('--git', 'create a git repository')
|
|
529
613
|
.option('--no-git', 'do not create a git repository')
|
|
530
614
|
.option(
|
|
@@ -581,18 +665,266 @@ export function cli({
|
|
|
581
665
|
await startDevWatchMode(projectName, devOptions)
|
|
582
666
|
})
|
|
583
667
|
|
|
584
|
-
// ===
|
|
668
|
+
// === LIBRARIES SUBCOMMAND ===
|
|
585
669
|
program
|
|
586
|
-
.command('
|
|
587
|
-
.description('
|
|
588
|
-
.option(
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
670
|
+
.command('libraries')
|
|
671
|
+
.description('List TanStack libraries')
|
|
672
|
+
.option(
|
|
673
|
+
'--group <group>',
|
|
674
|
+
`filter by group (${LIBRARY_GROUPS.join(', ')})`,
|
|
675
|
+
)
|
|
676
|
+
.option('--json', 'output JSON for automation', false)
|
|
677
|
+
.action(async (options: { group?: string; json: boolean }) => {
|
|
678
|
+
try {
|
|
679
|
+
const data = await fetchLibraries()
|
|
680
|
+
let libraries = data.libraries
|
|
681
|
+
|
|
682
|
+
if (
|
|
683
|
+
options.group &&
|
|
684
|
+
Object.prototype.hasOwnProperty.call(data.groups, options.group)
|
|
685
|
+
) {
|
|
686
|
+
const groupIds = data.groups[options.group]
|
|
687
|
+
libraries = libraries.filter((lib) => groupIds.includes(lib.id))
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const groupName = options.group
|
|
691
|
+
? data.groupNames[options.group] || options.group
|
|
692
|
+
: 'All Libraries'
|
|
693
|
+
|
|
694
|
+
const payload = {
|
|
695
|
+
group: groupName,
|
|
696
|
+
count: libraries.length,
|
|
697
|
+
libraries: libraries.map((lib) => ({
|
|
698
|
+
id: lib.id,
|
|
699
|
+
name: lib.name,
|
|
700
|
+
tagline: lib.tagline,
|
|
701
|
+
description: lib.description,
|
|
702
|
+
frameworks: lib.frameworks,
|
|
703
|
+
latestVersion: lib.latestVersion,
|
|
704
|
+
docsUrl: lib.docsUrl,
|
|
705
|
+
githubUrl: lib.githubUrl,
|
|
706
|
+
})),
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (options.json) {
|
|
710
|
+
printJson(payload)
|
|
711
|
+
return
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
console.log(chalk.bold(groupName))
|
|
715
|
+
for (const lib of payload.libraries) {
|
|
716
|
+
console.log(
|
|
717
|
+
`${chalk.bold(lib.id)} (${lib.latestVersion}) - ${lib.tagline}`,
|
|
718
|
+
)
|
|
719
|
+
}
|
|
720
|
+
} catch (error) {
|
|
721
|
+
log.error(error instanceof Error ? error.message : String(error))
|
|
722
|
+
process.exit(1)
|
|
723
|
+
}
|
|
594
724
|
})
|
|
595
725
|
|
|
726
|
+
// === DOC SUBCOMMAND ===
|
|
727
|
+
program
|
|
728
|
+
.command('doc')
|
|
729
|
+
.description('Fetch a TanStack documentation page')
|
|
730
|
+
.argument('<library>', 'library ID (eg. query, router, table)')
|
|
731
|
+
.argument('<path>', 'documentation path (eg. framework/react/overview)')
|
|
732
|
+
.option('--docs-version <version>', 'docs version (default: latest)', 'latest')
|
|
733
|
+
.option('--json', 'output JSON for automation', false)
|
|
734
|
+
.action(
|
|
735
|
+
async (
|
|
736
|
+
libraryId: string,
|
|
737
|
+
path: string,
|
|
738
|
+
options: { docsVersion: string; json: boolean },
|
|
739
|
+
) => {
|
|
740
|
+
try {
|
|
741
|
+
const data = await fetchLibraries()
|
|
742
|
+
const library = data.libraries.find((l) => l.id === libraryId)
|
|
743
|
+
|
|
744
|
+
if (!library) {
|
|
745
|
+
throw new Error(
|
|
746
|
+
`Library "${libraryId}" not found. Use \`tanstack libraries\` to see available libraries.`,
|
|
747
|
+
)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (
|
|
751
|
+
options.docsVersion !== 'latest' &&
|
|
752
|
+
!library.availableVersions.includes(options.docsVersion)
|
|
753
|
+
) {
|
|
754
|
+
throw new Error(
|
|
755
|
+
`Version "${options.docsVersion}" not found for ${library.name}. Available: ${library.availableVersions.join(', ')}`,
|
|
756
|
+
)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const branch =
|
|
760
|
+
options.docsVersion === 'latest' ||
|
|
761
|
+
options.docsVersion === library.latestVersion
|
|
762
|
+
? library.latestBranch || 'main'
|
|
763
|
+
: options.docsVersion
|
|
764
|
+
|
|
765
|
+
const docsRoot = library.docsRoot || 'docs'
|
|
766
|
+
const filePath = `${docsRoot}/${path}.md`
|
|
767
|
+
const content = await fetchDocContent(library.repo, branch, filePath)
|
|
768
|
+
|
|
769
|
+
if (!content) {
|
|
770
|
+
throw new Error(
|
|
771
|
+
`Document not found: ${library.name} / ${path} (version: ${options.docsVersion})`,
|
|
772
|
+
)
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
776
|
+
let title = path.split('/').pop() || 'Untitled'
|
|
777
|
+
let docContent = content
|
|
778
|
+
|
|
779
|
+
if (frontmatterMatch && frontmatterMatch[1]) {
|
|
780
|
+
const frontmatter = frontmatterMatch[1]
|
|
781
|
+
const titleMatch = frontmatter.match(
|
|
782
|
+
/title:\s*['"]?([^'"\n]+)['"]?/,
|
|
783
|
+
)
|
|
784
|
+
if (titleMatch && titleMatch[1]) {
|
|
785
|
+
title = titleMatch[1]
|
|
786
|
+
}
|
|
787
|
+
docContent = content.slice(frontmatterMatch[0].length).trim()
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const payload = {
|
|
791
|
+
title,
|
|
792
|
+
content: docContent,
|
|
793
|
+
url: `https://tanstack.com/${libraryId}/${options.docsVersion}/docs/${path}`,
|
|
794
|
+
library: library.name,
|
|
795
|
+
version:
|
|
796
|
+
options.docsVersion === 'latest'
|
|
797
|
+
? library.latestVersion
|
|
798
|
+
: options.docsVersion,
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (options.json) {
|
|
802
|
+
printJson(payload)
|
|
803
|
+
return
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
console.log(chalk.bold(payload.title))
|
|
807
|
+
console.log(chalk.blue(payload.url))
|
|
808
|
+
console.log('')
|
|
809
|
+
console.log(payload.content)
|
|
810
|
+
} catch (error) {
|
|
811
|
+
log.error(error instanceof Error ? error.message : String(error))
|
|
812
|
+
process.exit(1)
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
// === SEARCH-DOCS SUBCOMMAND ===
|
|
818
|
+
program
|
|
819
|
+
.command('search-docs')
|
|
820
|
+
.description('Search TanStack documentation')
|
|
821
|
+
.argument('<query>', 'search query')
|
|
822
|
+
.option('--library <id>', 'filter to specific library')
|
|
823
|
+
.option('--framework <name>', 'filter to specific framework')
|
|
824
|
+
.option('--limit <n>', 'max results (default: 10, max: 50)', parsePositiveInteger, 10)
|
|
825
|
+
.option('--json', 'output JSON for automation', false)
|
|
826
|
+
.action(
|
|
827
|
+
async (
|
|
828
|
+
query: string,
|
|
829
|
+
options: {
|
|
830
|
+
library?: string
|
|
831
|
+
framework?: string
|
|
832
|
+
limit: number
|
|
833
|
+
json: boolean
|
|
834
|
+
},
|
|
835
|
+
) => {
|
|
836
|
+
try {
|
|
837
|
+
const payload = await searchTanStackDocs({
|
|
838
|
+
query,
|
|
839
|
+
library: options.library,
|
|
840
|
+
framework: options.framework,
|
|
841
|
+
limit: options.limit,
|
|
842
|
+
})
|
|
843
|
+
|
|
844
|
+
if (options.json) {
|
|
845
|
+
printJson(payload)
|
|
846
|
+
return
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
for (const result of payload.results) {
|
|
850
|
+
console.log(
|
|
851
|
+
`${chalk.bold(result.title)} [${result.library}]\n${chalk.blue(result.url)}\n${result.snippet}\n`,
|
|
852
|
+
)
|
|
853
|
+
}
|
|
854
|
+
} catch (error) {
|
|
855
|
+
log.error(error instanceof Error ? error.message : String(error))
|
|
856
|
+
process.exit(1)
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
// === ECOSYSTEM SUBCOMMAND ===
|
|
862
|
+
program
|
|
863
|
+
.command('ecosystem')
|
|
864
|
+
.description('List TanStack ecosystem partners')
|
|
865
|
+
.option('--category <category>', 'filter by category')
|
|
866
|
+
.option('--library <id>', 'filter by TanStack library')
|
|
867
|
+
.option('--json', 'output JSON for automation', false)
|
|
868
|
+
.action(
|
|
869
|
+
async (options: { category?: string; library?: string; json: boolean }) => {
|
|
870
|
+
try {
|
|
871
|
+
const data = await fetchPartners()
|
|
872
|
+
|
|
873
|
+
let resolvedCategory: string | undefined
|
|
874
|
+
if (options.category) {
|
|
875
|
+
const normalized = options.category.toLowerCase().trim()
|
|
876
|
+
resolvedCategory = categoryAliases[normalized] || normalized
|
|
877
|
+
if (!data.categories.includes(resolvedCategory)) {
|
|
878
|
+
resolvedCategory = undefined
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const library = options.library?.toLowerCase().trim()
|
|
883
|
+
const partners = data.partners
|
|
884
|
+
.filter((partner) =>
|
|
885
|
+
resolvedCategory ? partner.category === resolvedCategory : true,
|
|
886
|
+
)
|
|
887
|
+
.filter((partner) =>
|
|
888
|
+
library ? partner.libraries.some((l) => l === library) : true,
|
|
889
|
+
)
|
|
890
|
+
.map((partner) => ({
|
|
891
|
+
id: partner.id,
|
|
892
|
+
name: partner.name,
|
|
893
|
+
tagline: partner.tagline,
|
|
894
|
+
description: partner.description,
|
|
895
|
+
category: partner.category,
|
|
896
|
+
categoryLabel: partner.categoryLabel,
|
|
897
|
+
url: partner.url,
|
|
898
|
+
libraries: partner.libraries,
|
|
899
|
+
}))
|
|
900
|
+
|
|
901
|
+
const payload = {
|
|
902
|
+
query: {
|
|
903
|
+
category: options.category,
|
|
904
|
+
categoryResolved: resolvedCategory,
|
|
905
|
+
library: options.library,
|
|
906
|
+
},
|
|
907
|
+
count: partners.length,
|
|
908
|
+
partners,
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (options.json) {
|
|
912
|
+
printJson(payload)
|
|
913
|
+
return
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
for (const partner of partners) {
|
|
917
|
+
console.log(
|
|
918
|
+
`${chalk.bold(partner.name)} [${partner.category}] - ${partner.description}\n${chalk.blue(partner.url)}`,
|
|
919
|
+
)
|
|
920
|
+
}
|
|
921
|
+
} catch (error) {
|
|
922
|
+
log.error(error instanceof Error ? error.message : String(error))
|
|
923
|
+
process.exit(1)
|
|
924
|
+
}
|
|
925
|
+
},
|
|
926
|
+
)
|
|
927
|
+
|
|
596
928
|
// === PIN-VERSIONS SUBCOMMAND ===
|
|
597
929
|
program
|
|
598
930
|
.command('pin-versions')
|
package/src/command-line.ts
CHANGED
|
@@ -52,6 +52,14 @@ function slugifyStarterName(value: string) {
|
|
|
52
52
|
.replace(/^-+|-+$/g, '')
|
|
53
53
|
}
|
|
54
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
|
+
|
|
55
63
|
function isLikelyStarterUrlOrPath(value: string) {
|
|
56
64
|
return (
|
|
57
65
|
/^https?:\/\//i.test(value) ||
|
|
@@ -88,7 +96,10 @@ function getStarterIdsFromUrl(starterUrl: string) {
|
|
|
88
96
|
return ids
|
|
89
97
|
}
|
|
90
98
|
|
|
91
|
-
function resolveMonorepoStarterById(
|
|
99
|
+
function resolveMonorepoStarterById(
|
|
100
|
+
starterId: string,
|
|
101
|
+
preferredFramework?: string,
|
|
102
|
+
) {
|
|
92
103
|
const normalized = starterId.toLowerCase().trim()
|
|
93
104
|
const idVariants = Array.from(
|
|
94
105
|
new Set([
|
|
@@ -106,8 +117,12 @@ function resolveMonorepoStarterById(starterId: string) {
|
|
|
106
117
|
resolve(cwd, '../../..'),
|
|
107
118
|
]
|
|
108
119
|
|
|
120
|
+
const frameworkOrder = preferredFramework
|
|
121
|
+
? [preferredFramework, ...['react', 'solid'].filter((f) => f !== preferredFramework)]
|
|
122
|
+
: ['react', 'solid']
|
|
123
|
+
|
|
109
124
|
for (const root of rootCandidates) {
|
|
110
|
-
for (const framework of
|
|
125
|
+
for (const framework of frameworkOrder) {
|
|
111
126
|
for (const id of idVariants) {
|
|
112
127
|
const templatePath = resolve(root, 'examples', framework, id, 'template.json')
|
|
113
128
|
if (fs.existsSync(templatePath)) {
|
|
@@ -125,7 +140,10 @@ function resolveMonorepoStarterById(starterId: string) {
|
|
|
125
140
|
return undefined
|
|
126
141
|
}
|
|
127
142
|
|
|
128
|
-
async function resolveStarterSpecifier(
|
|
143
|
+
export async function resolveStarterSpecifier(
|
|
144
|
+
starterSpecifier: string,
|
|
145
|
+
preferredFramework?: string,
|
|
146
|
+
) {
|
|
129
147
|
const normalized = starterSpecifier.trim()
|
|
130
148
|
|
|
131
149
|
if (!normalized || isLikelyStarterUrlOrPath(normalized)) {
|
|
@@ -135,7 +153,7 @@ async function resolveStarterSpecifier(starterSpecifier: string) {
|
|
|
135
153
|
const registry = await getRawRegistry()
|
|
136
154
|
if (registry && registry.starters?.length) {
|
|
137
155
|
const lookup = normalized.toLowerCase()
|
|
138
|
-
const
|
|
156
|
+
const matches = registry.starters.filter((starter) => {
|
|
139
157
|
const candidateIds = new Set<string>()
|
|
140
158
|
candidateIds.add(starter.name.toLowerCase())
|
|
141
159
|
candidateIds.add(slugifyStarterName(starter.name))
|
|
@@ -147,12 +165,25 @@ async function resolveStarterSpecifier(starterSpecifier: string) {
|
|
|
147
165
|
return candidateIds.has(lookup)
|
|
148
166
|
})
|
|
149
167
|
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
152
180
|
}
|
|
153
181
|
}
|
|
154
182
|
|
|
155
|
-
const monorepoStarterPath = resolveMonorepoStarterById(
|
|
183
|
+
const monorepoStarterPath = resolveMonorepoStarterById(
|
|
184
|
+
normalized,
|
|
185
|
+
preferredFramework,
|
|
186
|
+
)
|
|
156
187
|
if (monorepoStarterPath) {
|
|
157
188
|
return monorepoStarterPath
|
|
158
189
|
}
|
|
@@ -180,6 +211,110 @@ async function resolveStarterSpecifier(starterSpecifier: string) {
|
|
|
180
211
|
)
|
|
181
212
|
}
|
|
182
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
|
+
|
|
183
318
|
export function validateLegacyCreateFlags(cliOptions: CliOptions): {
|
|
184
319
|
warnings: Array<string>
|
|
185
320
|
error?: string
|
|
@@ -317,8 +452,12 @@ export async function normalizeOptions(
|
|
|
317
452
|
cliOptions.starter = cliOptions.templateId
|
|
318
453
|
}
|
|
319
454
|
|
|
455
|
+
const preferredFramework = (cliOptions.framework || 'react').toLowerCase()
|
|
456
|
+
|
|
320
457
|
const starter = !routerOnly && cliOptions.starter
|
|
321
|
-
? await loadStarter(
|
|
458
|
+
? await loadStarter(
|
|
459
|
+
await resolveStarterSpecifier(cliOptions.starter, preferredFramework),
|
|
460
|
+
)
|
|
322
461
|
: undefined
|
|
323
462
|
|
|
324
463
|
// TypeScript and Tailwind are always enabled with TanStack Start
|