create-tsrouter-app 0.2.0 → 0.3.0-alpha.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.
Files changed (72) hide show
  1. package/dist/add-ons.js +53 -0
  2. package/dist/cli.js +51 -0
  3. package/dist/constants.js +2 -0
  4. package/dist/create-app.js +285 -0
  5. package/dist/index.js +2 -347
  6. package/dist/options.js +231 -0
  7. package/dist/types.js +1 -0
  8. package/package.json +4 -3
  9. package/src/add-ons.ts +141 -0
  10. package/src/cli.ts +74 -0
  11. package/src/constants.ts +2 -0
  12. package/src/create-app.ts +445 -0
  13. package/src/index.ts +2 -507
  14. package/src/options.ts +264 -0
  15. package/src/types.ts +24 -0
  16. package/templates/add-on/clerk/README.md +3 -0
  17. package/templates/add-on/clerk/assets/.env.local.append +2 -0
  18. package/templates/add-on/clerk/assets/src/routes/demo.clerk.tsx +20 -0
  19. package/templates/add-on/clerk/info.json +28 -0
  20. package/templates/add-on/clerk/package.json +5 -0
  21. package/templates/add-on/convex/README.md +4 -0
  22. package/templates/add-on/convex/assets/.cursorrules.append +93 -0
  23. package/templates/add-on/convex/assets/.env.local.append +3 -0
  24. package/templates/add-on/convex/assets/convex/products.ts +8 -0
  25. package/templates/add-on/convex/assets/convex/schema.ts +10 -0
  26. package/templates/add-on/convex/assets/src/routes/demo.convex.tsx +33 -0
  27. package/templates/add-on/convex/info.json +27 -0
  28. package/templates/add-on/convex/package.json +6 -0
  29. package/templates/add-on/form/assets/src/routes/demo.form.tsx +50 -0
  30. package/templates/add-on/form/info.json +12 -0
  31. package/templates/add-on/form/package.json +5 -0
  32. package/templates/add-on/netlify/README.md +11 -0
  33. package/templates/add-on/netlify/info.json +6 -0
  34. package/templates/add-on/react-query/assets/src/routes/demo.react-query.tsx +30 -0
  35. package/templates/add-on/react-query/info.json +30 -0
  36. package/templates/add-on/react-query/package.json +6 -0
  37. package/templates/add-on/sentry/assets/.cursorrules +22 -0
  38. package/templates/add-on/sentry/assets/.env.local.append +2 -0
  39. package/templates/add-on/sentry/assets/src/routes/demo.sentry.bad-server-func.tsx +29 -0
  40. package/templates/add-on/sentry/info.json +13 -0
  41. package/templates/add-on/sentry/package.json +5 -0
  42. package/templates/add-on/shadcn/README.md +7 -0
  43. package/templates/add-on/shadcn/info.json +10 -0
  44. package/templates/add-on/start/assets/app.config.ts +16 -0
  45. package/templates/add-on/start/assets/postcss.config.ts +5 -0
  46. package/templates/add-on/start/assets/src/api.ts +6 -0
  47. package/templates/add-on/start/assets/src/client.tsx +10 -0
  48. package/templates/add-on/start/assets/src/router.tsx.ejs +34 -0
  49. package/templates/add-on/start/assets/src/routes/api.demo-names.ts +11 -0
  50. package/templates/add-on/start/assets/src/routes/demo.start.api-request.tsx.ejs +33 -0
  51. package/templates/add-on/start/assets/src/routes/demo.start.server-funcs.tsx +49 -0
  52. package/templates/add-on/start/assets/src/ssr.tsx +12 -0
  53. package/templates/add-on/start/info.json +18 -0
  54. package/templates/add-on/start/package.json +14 -0
  55. package/templates/add-on/store/assets/src/lib/demo-store.ts +5 -0
  56. package/templates/add-on/store/assets/src/routes/demo.store.page1.tsx +30 -0
  57. package/templates/add-on/store/assets/src/routes/demo.store.page2.tsx +30 -0
  58. package/templates/add-on/store/info.json +16 -0
  59. package/templates/add-on/store/package.json +6 -0
  60. package/templates/base/README.md.ejs +9 -0
  61. package/templates/base/package.json +1 -0
  62. package/templates/base/{tsconfig.json → tsconfig.json.ejs} +5 -1
  63. package/templates/base/vite.config.js.ejs +8 -0
  64. package/templates/example/ai-chat/assets/.env.local.append +2 -0
  65. package/templates/example/ai-chat/assets/src/routes/example.ai-chat.tsx.ejs +81 -0
  66. package/templates/example/ai-chat/info.json +27 -0
  67. package/templates/example/ai-chat/package.json +1 -0
  68. package/templates/file-router/src/components/Header.tsx.ejs +27 -0
  69. package/templates/file-router/src/routes/__root.tsx.ejs +80 -0
  70. package/templates/file-router/src/routes/__root.tsx +0 -11
  71. /package/dist/{utils/getPackageManager.js → package-manager.js} +0 -0
  72. /package/src/{utils/getPackageManager.ts → package-manager.ts} +0 -0
package/src/add-ons.ts ADDED
@@ -0,0 +1,141 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { existsSync, readdirSync, statSync } from 'node:fs'
3
+ import { resolve } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ type BooleanVariable = {
7
+ name: string
8
+ default: boolean
9
+ description: string
10
+ type: 'boolean'
11
+ }
12
+
13
+ type NumberVariable = {
14
+ name: string
15
+ default: number
16
+ description: string
17
+ type: 'number'
18
+ }
19
+
20
+ type StringVariable = {
21
+ name: string
22
+ default: string
23
+ description: string
24
+ type: 'string'
25
+ }
26
+
27
+ export type Variable = BooleanVariable | NumberVariable | StringVariable
28
+
29
+ export type AddOn = {
30
+ id: string
31
+ type: 'add-on' | 'example'
32
+ name: string
33
+ description: string
34
+ link: string
35
+ main?: Array<{
36
+ imports: Array<string>
37
+ initialize: Array<string>
38
+ providers: Array<{
39
+ open: string
40
+ close: string
41
+ }>
42
+ }>
43
+ layout?: {
44
+ imports: Array<string>
45
+ jsx: string
46
+ }
47
+ routes: Array<{
48
+ url: string
49
+ name: string
50
+ }>
51
+ userUi?: {
52
+ import: string
53
+ jsx: string
54
+ }
55
+ directory: string
56
+ packageAdditions: {
57
+ dependencies?: Record<string, string>
58
+ devDependencies?: Record<string, string>
59
+ scripts?: Record<string, string>
60
+ }
61
+ command?: {
62
+ command: string
63
+ args?: Array<string>
64
+ }
65
+ readme?: string
66
+ phase: 'setup' | 'add-on'
67
+ shadcnComponents?: Array<string>
68
+ warning?: string
69
+ dependsOn?: Array<string>
70
+ variables?: Array<Variable>
71
+ }
72
+
73
+ function isDirectory(path: string): boolean {
74
+ return statSync(path).isDirectory()
75
+ }
76
+
77
+ export async function getAllAddOns(): Promise<Array<AddOn>> {
78
+ const addOns: Array<AddOn> = []
79
+
80
+ for (const type of ['add-on', 'example']) {
81
+ const addOnsBase = fileURLToPath(
82
+ new URL(`../templates/${type}`, import.meta.url),
83
+ )
84
+
85
+ for (const dir of await readdirSync(addOnsBase).filter((file) =>
86
+ isDirectory(resolve(addOnsBase, file)),
87
+ )) {
88
+ const filePath = resolve(addOnsBase, dir, 'info.json')
89
+ const fileContent = await readFile(filePath, 'utf-8')
90
+
91
+ let packageAdditions: Record<string, string> = {}
92
+ if (existsSync(resolve(addOnsBase, dir, 'package.json'))) {
93
+ packageAdditions = JSON.parse(
94
+ await readFile(resolve(addOnsBase, dir, 'package.json'), 'utf-8'),
95
+ )
96
+ }
97
+
98
+ let readme: string | undefined
99
+ if (existsSync(resolve(addOnsBase, dir, 'README.md'))) {
100
+ readme = await readFile(resolve(addOnsBase, dir, 'README.md'), 'utf-8')
101
+ }
102
+
103
+ addOns.push({
104
+ id: dir,
105
+ type,
106
+ ...JSON.parse(fileContent),
107
+ directory: resolve(addOnsBase, dir),
108
+ packageAdditions,
109
+ readme,
110
+ })
111
+ }
112
+ }
113
+
114
+ return addOns
115
+ }
116
+
117
+ // Turn the list of chosen add-on IDs into a final list of add-ons by resolving dependencies
118
+ export async function finalizeAddOns(
119
+ chosenAddOnIDs: Array<string>,
120
+ ): Promise<Array<AddOn>> {
121
+ const finalAddOnIDs = new Set(chosenAddOnIDs)
122
+
123
+ const addOns = await getAllAddOns()
124
+
125
+ for (const addOnID of finalAddOnIDs) {
126
+ const addOn = addOns.find((a) => a.id === addOnID)
127
+ if (!addOn) {
128
+ throw new Error(`Add-on ${addOnID} not found`)
129
+ }
130
+
131
+ for (const dependsOn of addOn.dependsOn || []) {
132
+ const dep = addOns.find((a) => a.id === dependsOn)
133
+ if (!dep) {
134
+ throw new Error(`Dependency ${dependsOn} not found`)
135
+ }
136
+ finalAddOnIDs.add(dep.id)
137
+ }
138
+ }
139
+
140
+ return [...finalAddOnIDs].map((id) => addOns.find((a) => a.id === id)!)
141
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,74 @@
1
+ import { Command, InvalidArgumentError } from 'commander'
2
+ import { intro, log } from '@clack/prompts'
3
+
4
+ import { createApp } from './create-app.js'
5
+ import { normalizeOptions, promptForOptions } from './options.js'
6
+ import { SUPPORTED_PACKAGE_MANAGERS } from './package-manager.js'
7
+
8
+ import type { PackageManager } from './package-manager.js'
9
+ import type { CliOptions } from './types.js'
10
+
11
+ export function cli() {
12
+ const program = new Command()
13
+
14
+ program
15
+ .name('create-tsrouter-app')
16
+ .description('CLI to create a new TanStack application')
17
+ .argument('[project-name]', 'name of the project')
18
+ .option('--no-git', 'do not create a git repository')
19
+ .option<'typescript' | 'javascript' | 'file-router'>(
20
+ '--template <type>',
21
+ 'project template (typescript, javascript, file-router)',
22
+ (value) => {
23
+ if (
24
+ value !== 'typescript' &&
25
+ value !== 'javascript' &&
26
+ value !== 'file-router'
27
+ ) {
28
+ throw new InvalidArgumentError(
29
+ `Invalid template: ${value}. Only the following are allowed: typescript, javascript, file-router`,
30
+ )
31
+ }
32
+ return value
33
+ },
34
+ )
35
+ .option<PackageManager>(
36
+ `--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`,
37
+ `Explicitly tell the CLI to use this package manager`,
38
+ (value) => {
39
+ if (!SUPPORTED_PACKAGE_MANAGERS.includes(value as PackageManager)) {
40
+ throw new InvalidArgumentError(
41
+ `Invalid package manager: ${value}. The following are allowed: ${SUPPORTED_PACKAGE_MANAGERS.join(
42
+ ', ',
43
+ )}`,
44
+ )
45
+ }
46
+ return value as PackageManager
47
+ },
48
+ )
49
+ .option('--tailwind', 'add Tailwind CSS', false)
50
+ .option('--add-ons', 'pick from a list of available add-ons', false)
51
+ .action(async (projectName: string, options: CliOptions) => {
52
+ try {
53
+ const cliOptions = {
54
+ projectName,
55
+ ...options,
56
+ } as CliOptions
57
+ let finalOptions = normalizeOptions(cliOptions)
58
+ if (finalOptions) {
59
+ intro(`Creating a new TanStack app in ${projectName}...`)
60
+ } else {
61
+ intro("Let's configure your TanStack application")
62
+ finalOptions = await promptForOptions(cliOptions)
63
+ }
64
+ await createApp(finalOptions)
65
+ } catch (error) {
66
+ log.error(
67
+ error instanceof Error ? error.message : 'An unknown error occurred',
68
+ )
69
+ process.exit(1)
70
+ }
71
+ })
72
+
73
+ program.parse()
74
+ }
@@ -0,0 +1,2 @@
1
+ export const CODE_ROUTER = 'code-router'
2
+ export const FILE_ROUTER = 'file-router'
@@ -0,0 +1,445 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ appendFile,
5
+ copyFile,
6
+ mkdir,
7
+ readFile,
8
+ writeFile,
9
+ } from 'node:fs/promises'
10
+ import { existsSync, readdirSync, statSync } from 'node:fs'
11
+ import { basename, dirname, resolve } from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+ import { log, outro, spinner } from '@clack/prompts'
14
+ import { execa } from 'execa'
15
+ import { render } from 'ejs'
16
+ import { format } from 'prettier'
17
+ import chalk from 'chalk'
18
+
19
+ import { CODE_ROUTER, FILE_ROUTER } from './constants.js'
20
+
21
+ import type { Options } from './types.js'
22
+
23
+ function sortObject(obj: Record<string, string>): Record<string, string> {
24
+ return Object.keys(obj)
25
+ .sort()
26
+ .reduce<Record<string, string>>((acc, key) => {
27
+ acc[key] = obj[key]
28
+ return acc
29
+ }, {})
30
+ }
31
+
32
+ function createCopyFiles(targetDir: string) {
33
+ return async function copyFiles(templateDir: string, files: Array<string>) {
34
+ for (const file of files) {
35
+ const targetFileName = file.replace('.tw', '')
36
+ await copyFile(
37
+ resolve(templateDir, file),
38
+ resolve(targetDir, targetFileName),
39
+ )
40
+ }
41
+ }
42
+ }
43
+
44
+ function createTemplateFile(
45
+ projectName: string,
46
+ options: Required<Options>,
47
+ targetDir: string,
48
+ ) {
49
+ return async function templateFile(
50
+ templateDir: string,
51
+ file: string,
52
+ targetFileName?: string,
53
+ ) {
54
+ const templateValues = {
55
+ packageManager: options.packageManager,
56
+ projectName: projectName,
57
+ typescript: options.typescript,
58
+ tailwind: options.tailwind,
59
+ js: options.typescript ? 'ts' : 'js',
60
+ jsx: options.typescript ? 'tsx' : 'jsx',
61
+ fileRouter: options.mode === FILE_ROUTER,
62
+ codeRouter: options.mode === CODE_ROUTER,
63
+ addOnEnabled: options.chosenAddOns.reduce<Record<string, boolean>>(
64
+ (acc, addOn) => {
65
+ acc[addOn.id] = true
66
+ return acc
67
+ },
68
+ {},
69
+ ),
70
+ addOns: options.chosenAddOns,
71
+ }
72
+
73
+ const template = await readFile(resolve(templateDir, file), 'utf-8')
74
+ let content = render(template, templateValues)
75
+ const target = targetFileName ?? file.replace('.ejs', '')
76
+
77
+ if (target.endsWith('.ts') || target.endsWith('.tsx')) {
78
+ content = await format(content, {
79
+ semi: false,
80
+ singleQuote: true,
81
+ trailingComma: 'all',
82
+ parser: 'typescript',
83
+ })
84
+ }
85
+
86
+ await mkdir(dirname(resolve(targetDir, target)), {
87
+ recursive: true,
88
+ })
89
+
90
+ await writeFile(resolve(targetDir, target), content)
91
+ }
92
+ }
93
+
94
+ async function createPackageJSON(
95
+ projectName: string,
96
+ options: Required<Options>,
97
+ templateDir: string,
98
+ routerDir: string,
99
+ targetDir: string,
100
+ addOns: Array<{
101
+ dependencies?: Record<string, string>
102
+ devDependencies?: Record<string, string>
103
+ scripts?: Record<string, string>
104
+ }>,
105
+ ) {
106
+ let packageJSON = JSON.parse(
107
+ await readFile(resolve(templateDir, 'package.json'), 'utf8'),
108
+ )
109
+ packageJSON.name = projectName
110
+ if (options.typescript) {
111
+ const tsPackageJSON = JSON.parse(
112
+ await readFile(resolve(templateDir, 'package.ts.json'), 'utf8'),
113
+ )
114
+ packageJSON = {
115
+ ...packageJSON,
116
+ devDependencies: {
117
+ ...packageJSON.devDependencies,
118
+ ...tsPackageJSON.devDependencies,
119
+ },
120
+ }
121
+ }
122
+ if (options.tailwind) {
123
+ const twPackageJSON = JSON.parse(
124
+ await readFile(resolve(templateDir, 'package.tw.json'), 'utf8'),
125
+ )
126
+ packageJSON = {
127
+ ...packageJSON,
128
+ dependencies: {
129
+ ...packageJSON.dependencies,
130
+ ...twPackageJSON.dependencies,
131
+ },
132
+ }
133
+ }
134
+ if (options.mode === FILE_ROUTER) {
135
+ const frPackageJSON = JSON.parse(
136
+ await readFile(resolve(routerDir, 'package.fr.json'), 'utf8'),
137
+ )
138
+ packageJSON = {
139
+ ...packageJSON,
140
+ dependencies: {
141
+ ...packageJSON.dependencies,
142
+ ...frPackageJSON.dependencies,
143
+ },
144
+ }
145
+ }
146
+
147
+ for (const addOn of addOns) {
148
+ packageJSON = {
149
+ ...packageJSON,
150
+ dependencies: {
151
+ ...packageJSON.dependencies,
152
+ ...addOn.dependencies,
153
+ },
154
+ devDependencies: {
155
+ ...packageJSON.devDependencies,
156
+ ...addOn.devDependencies,
157
+ },
158
+ scripts: {
159
+ ...packageJSON.scripts,
160
+ ...addOn.scripts,
161
+ },
162
+ }
163
+ }
164
+
165
+ packageJSON.dependencies = sortObject(
166
+ packageJSON.dependencies as Record<string, string>,
167
+ )
168
+ packageJSON.devDependencies = sortObject(
169
+ packageJSON.devDependencies as Record<string, string>,
170
+ )
171
+
172
+ await writeFile(
173
+ resolve(targetDir, 'package.json'),
174
+ JSON.stringify(packageJSON, null, 2),
175
+ )
176
+ }
177
+
178
+ async function copyFilesRecursively(
179
+ source: string,
180
+ target: string,
181
+ copyFile: (source: string, target: string) => Promise<void>,
182
+ templateFile: (file: string, targetFileName?: string) => Promise<void>,
183
+ ) {
184
+ const sourceStat = statSync(source)
185
+ if (sourceStat.isDirectory()) {
186
+ const files = readdirSync(source)
187
+ for (const file of files) {
188
+ const sourceChild = resolve(source, file)
189
+ const targetChild = resolve(target, file)
190
+ await copyFilesRecursively(
191
+ sourceChild,
192
+ targetChild,
193
+ copyFile,
194
+ templateFile,
195
+ )
196
+ }
197
+ } else {
198
+ if (source.endsWith('.ejs')) {
199
+ const targetPath = target.replace('.ejs', '')
200
+ await mkdir(dirname(targetPath), {
201
+ recursive: true,
202
+ })
203
+ await templateFile(source, targetPath)
204
+ } else {
205
+ await mkdir(dirname(target), {
206
+ recursive: true,
207
+ })
208
+ if (source.endsWith('.append')) {
209
+ await appendFile(
210
+ target.replace('.append', ''),
211
+ (await readFile(source)).toString(),
212
+ )
213
+ } else {
214
+ await copyFile(source, target)
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ export async function createApp(options: Required<Options>) {
221
+ const templateDirBase = fileURLToPath(
222
+ new URL('../templates/base', import.meta.url),
223
+ )
224
+ const templateDirRouter = fileURLToPath(
225
+ new URL(`../templates/${options.mode}`, import.meta.url),
226
+ )
227
+ const targetDir = resolve(process.cwd(), options.projectName)
228
+
229
+ if (existsSync(targetDir)) {
230
+ log.error(`Directory "${options.projectName}" already exists`)
231
+ return
232
+ }
233
+
234
+ const copyFiles = createCopyFiles(targetDir)
235
+ const templateFile = createTemplateFile(
236
+ options.projectName,
237
+ options,
238
+ targetDir,
239
+ )
240
+
241
+ const isAddOnEnabled = (id: string) =>
242
+ options.chosenAddOns.find((a) => a.id === id)
243
+
244
+ log.info(`Creating a new TanStack app in '${basename(targetDir)}'...`)
245
+
246
+ // Make the root directory
247
+ await mkdir(targetDir, { recursive: true })
248
+
249
+ // Setup the .vscode directory
250
+ await mkdir(resolve(targetDir, '.vscode'), { recursive: true })
251
+ await copyFile(
252
+ resolve(templateDirBase, '.vscode/settings.json'),
253
+ resolve(targetDir, '.vscode/settings.json'),
254
+ )
255
+
256
+ // Fill the public directory
257
+ await mkdir(resolve(targetDir, 'public'), { recursive: true })
258
+ copyFiles(templateDirBase, [
259
+ './public/robots.txt',
260
+ './public/favicon.ico',
261
+ './public/manifest.json',
262
+ './public/logo192.png',
263
+ './public/logo512.png',
264
+ ])
265
+
266
+ // Make the src directory
267
+ await mkdir(resolve(targetDir, 'src'), { recursive: true })
268
+ if (options.mode === FILE_ROUTER) {
269
+ await mkdir(resolve(targetDir, 'src/routes'), { recursive: true })
270
+ await mkdir(resolve(targetDir, 'src/components'), { recursive: true })
271
+ }
272
+
273
+ // Copy in Vite and Tailwind config and CSS
274
+ if (!options.tailwind) {
275
+ await copyFiles(templateDirBase, ['./src/App.css'])
276
+ }
277
+ await templateFile(templateDirBase, './vite.config.js.ejs')
278
+ await templateFile(templateDirBase, './src/styles.css.ejs')
279
+
280
+ copyFiles(templateDirBase, ['./src/logo.svg'])
281
+
282
+ // Setup the app component. There are four variations, typescript/javascript and tailwind/non-tailwind.
283
+ if (options.mode === FILE_ROUTER) {
284
+ await templateFile(
285
+ templateDirRouter,
286
+ './src/components/Header.tsx.ejs',
287
+ './src/components/Header.tsx',
288
+ )
289
+ await templateFile(
290
+ templateDirRouter,
291
+ './src/routes/__root.tsx.ejs',
292
+ './src/routes/__root.tsx',
293
+ )
294
+ await templateFile(
295
+ templateDirBase,
296
+ './src/App.tsx.ejs',
297
+ './src/routes/index.tsx',
298
+ )
299
+ } else {
300
+ await templateFile(
301
+ templateDirBase,
302
+ './src/App.tsx.ejs',
303
+ options.typescript ? undefined : './src/App.jsx',
304
+ )
305
+ await templateFile(
306
+ templateDirBase,
307
+ './src/App.test.tsx.ejs',
308
+ options.typescript ? undefined : './src/App.test.jsx',
309
+ )
310
+ }
311
+
312
+ // Create the main entry point
313
+ if (!isAddOnEnabled('start')) {
314
+ if (options.typescript) {
315
+ await templateFile(templateDirRouter, './src/main.tsx.ejs')
316
+ } else {
317
+ await templateFile(
318
+ templateDirRouter,
319
+ './src/main.tsx.ejs',
320
+ './src/main.jsx',
321
+ )
322
+ }
323
+ }
324
+
325
+ // Setup the main, reportWebVitals and index.html files
326
+ if (!isAddOnEnabled('start')) {
327
+ if (options.typescript) {
328
+ await templateFile(templateDirBase, './src/reportWebVitals.ts.ejs')
329
+ } else {
330
+ await templateFile(
331
+ templateDirBase,
332
+ './src/reportWebVitals.ts.ejs',
333
+ './src/reportWebVitals.js',
334
+ )
335
+ }
336
+ await templateFile(templateDirBase, './index.html.ejs')
337
+ }
338
+
339
+ // Setup tsconfig
340
+ if (options.typescript) {
341
+ await templateFile(
342
+ templateDirBase,
343
+ './tsconfig.json.ejs',
344
+ './tsconfig.json',
345
+ )
346
+ }
347
+
348
+ // Setup the package.json file, optionally with typescript and tailwind
349
+ await createPackageJSON(
350
+ options.projectName,
351
+ options,
352
+ templateDirBase,
353
+ templateDirRouter,
354
+ targetDir,
355
+ options.chosenAddOns.map((addOn) => addOn.packageAdditions),
356
+ )
357
+
358
+ // Copy all the asset files from the addons
359
+ const s = spinner()
360
+ for (const phase of ['setup', 'add-on', 'example']) {
361
+ for (const addOn of options.chosenAddOns.filter(
362
+ (addOn) => addOn.phase === phase,
363
+ )) {
364
+ s.start(`Setting up ${addOn.name}...`)
365
+ const addOnDir = resolve(addOn.directory, 'assets')
366
+ if (existsSync(addOnDir)) {
367
+ await copyFilesRecursively(
368
+ addOnDir,
369
+ targetDir,
370
+ copyFile,
371
+ async (file: string, targetFileName?: string) =>
372
+ templateFile(addOnDir, file, targetFileName),
373
+ )
374
+ }
375
+
376
+ if (addOn.command) {
377
+ await execa(addOn.command.command, addOn.command.args || [], {
378
+ cwd: targetDir,
379
+ })
380
+ }
381
+ s.stop(`${addOn.name} setup complete`)
382
+ }
383
+ }
384
+
385
+ if (isAddOnEnabled('shadcn')) {
386
+ const shadcnComponents = new Set<string>()
387
+ for (const addOn of options.chosenAddOns) {
388
+ if (addOn.shadcnComponents) {
389
+ for (const component of addOn.shadcnComponents) {
390
+ shadcnComponents.add(component)
391
+ }
392
+ }
393
+ }
394
+
395
+ if (shadcnComponents.size > 0) {
396
+ s.start(
397
+ `Installing shadcn components (${Array.from(shadcnComponents).join(', ')})...`,
398
+ )
399
+ await execa('npx', ['shadcn@canary', 'add', ...shadcnComponents], {
400
+ cwd: targetDir,
401
+ })
402
+ s.stop(`Installed shadcn components`)
403
+ }
404
+ }
405
+
406
+ const warnings: Array<string> = []
407
+ for (const addOn of options.chosenAddOns) {
408
+ if (addOn.warning) {
409
+ warnings.push(addOn.warning)
410
+ }
411
+ }
412
+
413
+ // Add .gitignore
414
+ await copyFile(
415
+ resolve(templateDirBase, 'gitignore'),
416
+ resolve(targetDir, '.gitignore'),
417
+ )
418
+
419
+ // Create the README.md
420
+ await templateFile(templateDirBase, 'README.md.ejs')
421
+
422
+ // Install dependencies
423
+ s.start(`Installing dependencies via ${options.packageManager}...`)
424
+ await execa(options.packageManager, ['install'], { cwd: targetDir })
425
+ s.stop(`Installed dependencies`)
426
+
427
+ if (warnings.length > 0) {
428
+ log.warn(chalk.red(warnings.join('\n')))
429
+ }
430
+
431
+ if (options.git) {
432
+ s.start(`Initializing git repository...`)
433
+ await execa('git', ['init'], { cwd: targetDir })
434
+ s.stop(`Initialized git repository`)
435
+ }
436
+
437
+ outro(`Created your new TanStack app in '${basename(targetDir)}'.
438
+
439
+ Use the following commands to start your app:
440
+ % cd ${options.projectName}
441
+ % ${options.packageManager} ${isAddOnEnabled('start') ? 'dev' : 'start'}
442
+
443
+ Please read README.md for more information on testing, styling, adding routes, react-query, etc.
444
+ `)
445
+ }