Skip to content

Commit 8c8dd83

Browse files
committed
feat: add-ons from the command line
1 parent ed4d300 commit 8c8dd83

File tree

6 files changed

+132
-77
lines changed

6 files changed

+132
-77
lines changed

src/add-ons.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { readFile } from 'node:fs/promises'
22
import { existsSync, readdirSync, statSync } from 'node:fs'
33
import { resolve } from 'node:path'
44
import { fileURLToPath } from 'node:url'
5+
import chalk from 'chalk'
56

6-
import type { Framework } from './types.js'
7+
import { DEFAULT_FRAMEWORK } from './constants.js'
8+
import type { CliOptions, Framework } from './types.js'
79

810
type BooleanVariable = {
911
name: string
@@ -140,3 +142,15 @@ export async function finalizeAddOns(
140142

141143
return [...finalAddOnIDs].map((id) => addOns.find((a) => a.id === id)!)
142144
}
145+
146+
export async function listAddOns(options: CliOptions) {
147+
const mode =
148+
options.template === 'file-router' ? 'file-router' : 'code-router'
149+
const addOns = await getAllAddOns(
150+
options.framework || DEFAULT_FRAMEWORK,
151+
mode,
152+
)
153+
for (const addOn of addOns) {
154+
console.log(`${chalk.bold(addOn.id)}: ${addOn.description}`)
155+
}
156+
}

src/cli.ts

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { Command, InvalidArgumentError } from 'commander'
22
import { intro, log } from '@clack/prompts'
3+
import chalk from 'chalk'
34

45
import { createApp } from './create-app.js'
56
import { normalizeOptions, promptForOptions } from './options.js'
67
import { SUPPORTED_PACKAGE_MANAGERS } from './package-manager.js'
78

9+
import { getAllAddOns, listAddOns } from './add-ons.js'
810
import { DEFAULT_FRAMEWORK, SUPPORTED_FRAMEWORKS } from './constants.js'
911
import type { PackageManager } from './package-manager.js'
1012
import type { CliOptions, Framework } from './types.js'
@@ -63,26 +65,44 @@ export function cli() {
6365
},
6466
)
6567
.option('--tailwind', 'add Tailwind CSS', false)
66-
.option('--add-ons', 'pick from a list of available add-ons', false)
68+
.option<Array<string> | boolean>(
69+
'--add-ons [...add-ons]',
70+
'pick from a list of available add-ons (comma separated list)',
71+
(value: string) => {
72+
let addOns: Array<string> | boolean = !!value
73+
if (typeof value === 'string') {
74+
addOns = value.split(',').map((addon) => addon.trim())
75+
}
76+
return addOns
77+
},
78+
)
79+
.option('--list-add-ons', 'list all available add-ons', false)
6780
.action(async (projectName: string, options: CliOptions) => {
68-
try {
69-
const cliOptions = {
70-
projectName,
71-
...options,
72-
} as CliOptions
73-
let finalOptions = normalizeOptions(cliOptions)
74-
if (finalOptions) {
75-
intro(`Creating a new TanStack app in ${projectName}...`)
76-
} else {
77-
intro("Let's configure your TanStack application")
78-
finalOptions = await promptForOptions(cliOptions)
81+
if (options.listAddOns) {
82+
await listAddOns(options)
83+
} else {
84+
try {
85+
const cliOptions = {
86+
projectName,
87+
...options,
88+
} as CliOptions
89+
90+
let finalOptions = await normalizeOptions(cliOptions)
91+
if (finalOptions) {
92+
intro(`Creating a new TanStack app in ${projectName}...`)
93+
} else {
94+
intro("Let's configure your TanStack application")
95+
finalOptions = await promptForOptions(cliOptions)
96+
}
97+
await createApp(finalOptions)
98+
} catch (error) {
99+
log.error(
100+
error instanceof Error
101+
? error.message
102+
: 'An unknown error occurred',
103+
)
104+
process.exit(1)
79105
}
80-
await createApp(finalOptions)
81-
} catch (error) {
82-
log.error(
83-
error instanceof Error ? error.message : 'An unknown error occurred',
84-
)
85-
process.exit(1)
86106
}
87107
})
88108

src/create-app.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,6 @@ export async function createApp(options: Required<Options>) {
268268
const isAddOnEnabled = (id: string) =>
269269
options.chosenAddOns.find((a) => a.id === id)
270270

271-
log.info(`Creating a new TanStack app in '${basename(targetDir)}'...`)
272-
273271
// Make the root directory
274272
await mkdir(targetDir, { recursive: true })
275273

@@ -478,7 +476,7 @@ export async function createApp(options: Required<Options>) {
478476
} else {
479477
await templateFile(
480478
templateDirRouter,
481-
'./src/main.jsx.ejs',
479+
'./src/main.tsx.ejs',
482480
'./src/main.jsx',
483481
{
484482
routes,

src/options.ts

Lines changed: 73 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,47 @@ import {
1414
} from './package-manager.js'
1515
import { CODE_ROUTER, DEFAULT_FRAMEWORK, FILE_ROUTER } from './constants.js'
1616
import { finalizeAddOns, getAllAddOns } from './add-ons.js'
17-
import type { Variable } from './add-ons.js'
17+
import type { AddOn, Variable } from './add-ons.js'
1818

1919
import type { CliOptions, Options } from './types.js'
2020

2121
// If all CLI options are provided, use them directly
22-
export function normalizeOptions(
22+
export async function normalizeOptions(
2323
cliOptions: CliOptions,
24-
): Required<Options> | undefined {
24+
): Promise<Required<Options> | undefined> {
2525
if (cliOptions.projectName) {
2626
const typescript =
2727
cliOptions.template === 'typescript' ||
2828
cliOptions.template === 'file-router' ||
2929
cliOptions.framework === 'solid'
3030

31-
const tailwind =
31+
let tailwind =
3232
cliOptions.tailwind === undefined
3333
? cliOptions.framework === 'solid'
3434
: cliOptions.tailwind
3535

36+
let addOns = false
37+
let chosenAddOns: Array<AddOn> = []
38+
if (Array.isArray(cliOptions.addOns)) {
39+
addOns = true
40+
chosenAddOns = await finalizeAddOns(
41+
cliOptions.framework || DEFAULT_FRAMEWORK,
42+
cliOptions.template === 'file-router' ? FILE_ROUTER : CODE_ROUTER,
43+
cliOptions.addOns,
44+
)
45+
tailwind = true
46+
}
47+
3648
return {
3749
framework: cliOptions.framework || 'react',
3850
projectName: cliOptions.projectName,
3951
typescript,
40-
tailwind: !!tailwind,
52+
tailwind,
4153
packageManager: cliOptions.packageManager || DEFAULT_PACKAGE_MANAGER,
4254
mode: cliOptions.template === 'file-router' ? FILE_ROUTER : CODE_ROUTER,
4355
git: !!cliOptions.git,
44-
addOns: !!cliOptions.addOns,
45-
chosenAddOns: [],
56+
addOns,
57+
chosenAddOns,
4658
variableValues: {},
4759
}
4860
}
@@ -206,58 +218,66 @@ export async function promptForOptions(
206218
options.packageManager = cliOptions.packageManager
207219
}
208220

209-
// Select any add-ons
210-
const allAddOns = await getAllAddOns(options.framework, options.mode)
211-
const addOns = allAddOns.filter((addOn) => addOn.type === 'add-on')
212-
let selectedAddOns: Array<string> = []
213-
if (options.typescript && cliOptions.addOns && addOns.length > 0) {
214-
const value = await multiselect({
215-
message: 'What add-ons would you like for your project:',
216-
options: addOns.map((addOn) => ({
217-
value: addOn.id,
218-
label: addOn.name,
219-
hint: addOn.description,
220-
})),
221-
required: false,
222-
})
221+
options.chosenAddOns = []
222+
if (Array.isArray(cliOptions.addOns)) {
223+
options.chosenAddOns = await finalizeAddOns(
224+
options.framework,
225+
options.mode,
226+
cliOptions.addOns,
227+
)
228+
options.tailwind = true
229+
} else if (cliOptions.addOns) {
230+
// Select any add-ons
231+
const allAddOns = await getAllAddOns(options.framework, options.mode)
232+
const addOns = allAddOns.filter((addOn) => addOn.type === 'add-on')
233+
let selectedAddOns: Array<string> = []
234+
if (options.typescript && addOns.length > 0) {
235+
const value = await multiselect({
236+
message: 'What add-ons would you like for your project:',
237+
options: addOns.map((addOn) => ({
238+
value: addOn.id,
239+
label: addOn.name,
240+
hint: addOn.description,
241+
})),
242+
required: false,
243+
})
223244

224-
if (isCancel(value)) {
225-
cancel('Operation cancelled.')
226-
process.exit(0)
245+
if (isCancel(value)) {
246+
cancel('Operation cancelled.')
247+
process.exit(0)
248+
}
249+
selectedAddOns = value
227250
}
228-
selectedAddOns = value
229-
}
230251

231-
// Select any examples
232-
const examples = allAddOns.filter((addOn) => addOn.type === 'example')
233-
let selectedExamples: Array<string> = []
234-
if (options.typescript && cliOptions.addOns && examples.length > 0) {
235-
const value = await multiselect({
236-
message: 'Would you like any examples?',
237-
options: examples.map((addOn) => ({
238-
value: addOn.id,
239-
label: addOn.name,
240-
hint: addOn.description,
241-
})),
242-
required: false,
243-
})
252+
// Select any examples
253+
const examples = allAddOns.filter((addOn) => addOn.type === 'example')
254+
let selectedExamples: Array<string> = []
255+
if (options.typescript && examples.length > 0) {
256+
const value = await multiselect({
257+
message: 'Would you like any examples?',
258+
options: examples.map((addOn) => ({
259+
value: addOn.id,
260+
label: addOn.name,
261+
hint: addOn.description,
262+
})),
263+
required: false,
264+
})
244265

245-
if (isCancel(value)) {
246-
cancel('Operation cancelled.')
247-
process.exit(0)
266+
if (isCancel(value)) {
267+
cancel('Operation cancelled.')
268+
process.exit(0)
269+
}
270+
selectedExamples = value
248271
}
249-
selectedExamples = value
250-
}
251272

252-
if (selectedAddOns.length > 0 || selectedExamples.length > 0) {
253-
options.chosenAddOns = await finalizeAddOns(
254-
options.framework,
255-
options.mode,
256-
[...selectedAddOns, ...selectedExamples],
257-
)
258-
options.tailwind = true
259-
} else {
260-
options.chosenAddOns = []
273+
if (selectedAddOns.length > 0 || selectedExamples.length > 0) {
274+
options.chosenAddOns = await finalizeAddOns(
275+
options.framework,
276+
options.mode,
277+
[...selectedAddOns, ...selectedExamples],
278+
)
279+
options.tailwind = true
280+
}
261281
}
262282

263283
// Collect variables

src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ export interface CliOptions {
2424
packageManager?: PackageManager
2525
projectName?: string
2626
git?: boolean
27-
addOns?: boolean
27+
addOns?: Array<string> | boolean
28+
listAddOns?: boolean
2829
}

templates/react/file-router/src/routes/__root.tsx.ejs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createRootRoute, Outlet<% if (addOnEnabled.start) { %>
22
,HeadContent, Scripts<% } %> } from '@tanstack/react-router'
33
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
4-
<% if (tailwind) { %>
4+
<% if (addOns.length) { %>
55
import Header from '../components/Header'
66
<% } %><% for(const integration of integrations.filter(i => i.type === 'layout' || i.type === 'provider')) { %>
77
import <%= integration.name %> from "../<%= integration.path %>";
@@ -39,7 +39,9 @@ export const Route = createRootRoute({
3939
<% for(const integration of integrations.filter(i => i.type === 'provider')) { %>
4040
<<%= integration.name %>>
4141
<% } %>
42+
<% if (addOns.length) { %>
4243
<Header />
44+
<% } %>
4345
<Outlet />
4446
<TanStackRouterDevtools />
4547
<% for(const integration of integrations.filter(i => i.type === 'layout')) { %>

0 commit comments

Comments
 (0)