create-start-app 0.3.1 → 0.4.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.
Files changed (166) hide show
  1. package/README.md +53 -9
  2. package/dist/add-ons.js +69 -0
  3. package/dist/cli.js +78 -0
  4. package/dist/constants.js +4 -0
  5. package/dist/create-app.js +371 -0
  6. package/dist/index.js +2 -347
  7. package/dist/mcp.js +169 -0
  8. package/dist/options.js +261 -0
  9. package/dist/{utils/getPackageManager.js → package-manager.js} +1 -0
  10. package/dist/types.js +1 -0
  11. package/images/mcp-configuration.png +0 -0
  12. package/package.json +8 -4
  13. package/src/add-ons.ts +156 -0
  14. package/src/cli.ts +114 -0
  15. package/src/constants.ts +7 -0
  16. package/src/create-app.ts +582 -0
  17. package/src/index.ts +2 -507
  18. package/src/mcp.ts +205 -0
  19. package/src/options.ts +309 -0
  20. package/src/{utils/getPackageManager.ts → package-manager.ts} +1 -0
  21. package/src/types.ts +30 -0
  22. package/templates/react/add-on/clerk/README.md +3 -0
  23. package/templates/react/add-on/clerk/assets/_dot_env.local.append +2 -0
  24. package/templates/react/add-on/clerk/assets/src/integrations/clerk/header-user.tsx +19 -0
  25. package/templates/react/add-on/clerk/assets/src/integrations/clerk/provider.tsx +18 -0
  26. package/templates/react/add-on/clerk/assets/src/routes/demo.clerk.tsx +20 -0
  27. package/templates/react/add-on/clerk/info.json +13 -0
  28. package/templates/react/add-on/clerk/package.json +5 -0
  29. package/templates/react/add-on/convex/README.md +4 -0
  30. package/templates/react/add-on/convex/assets/_dot_cursorrules.append +93 -0
  31. package/templates/react/add-on/convex/assets/_dot_env.local.append +3 -0
  32. package/templates/react/add-on/convex/assets/convex/products.ts +8 -0
  33. package/templates/react/add-on/convex/assets/convex/schema.ts +10 -0
  34. package/templates/react/add-on/convex/assets/src/integrations/convex/provider.tsx +20 -0
  35. package/templates/react/add-on/convex/assets/src/routes/demo.convex.tsx +33 -0
  36. package/templates/react/add-on/convex/info.json +13 -0
  37. package/templates/react/add-on/convex/package.json +6 -0
  38. package/templates/react/add-on/form/assets/src/routes/demo.form.tsx.ejs +62 -0
  39. package/templates/react/add-on/form/info.json +13 -0
  40. package/templates/react/add-on/form/package.json +5 -0
  41. package/templates/react/add-on/module-federation/assets/module-federation.config.js.ejs +31 -0
  42. package/templates/react/add-on/module-federation/assets/src/demo-mf-component.tsx +3 -0
  43. package/templates/react/add-on/module-federation/assets/src/demo-mf-self-contained.tsx +11 -0
  44. package/templates/react/add-on/module-federation/info.json +7 -0
  45. package/templates/react/add-on/module-federation/package.json +5 -0
  46. package/templates/react/add-on/netlify/README.md +11 -0
  47. package/templates/react/add-on/netlify/info.json +7 -0
  48. package/templates/react/add-on/sentry/assets/_dot_cursorrules.append +22 -0
  49. package/templates/react/add-on/sentry/assets/_dot_env.local.append +2 -0
  50. package/templates/react/add-on/sentry/assets/src/app/global-middleware.ts +25 -0
  51. package/templates/react/add-on/sentry/assets/src/routes/demo.sentry.testing.tsx +480 -0
  52. package/templates/react/add-on/sentry/info.json +14 -0
  53. package/templates/react/add-on/sentry/package.json +7 -0
  54. package/templates/react/add-on/shadcn/README.md +7 -0
  55. package/templates/react/add-on/shadcn/assets/_dot_cursorrules.append +7 -0
  56. package/templates/react/add-on/shadcn/info.json +11 -0
  57. package/templates/react/add-on/start/assets/app.config.ts +16 -0
  58. package/templates/react/add-on/start/assets/postcss.config.ts +5 -0
  59. package/templates/react/add-on/start/assets/src/api.ts +6 -0
  60. package/templates/react/add-on/start/assets/src/client.tsx +10 -0
  61. package/templates/react/add-on/start/assets/src/router.tsx.ejs +51 -0
  62. package/templates/react/add-on/start/assets/src/routes/api.demo-names.ts +11 -0
  63. package/templates/react/add-on/start/assets/src/routes/demo.start.api-request.tsx.ejs +33 -0
  64. package/templates/react/add-on/start/assets/src/routes/demo.start.server-funcs.tsx +49 -0
  65. package/templates/react/add-on/start/assets/src/ssr.tsx +12 -0
  66. package/templates/react/add-on/start/info.json +19 -0
  67. package/templates/react/add-on/start/package.json +14 -0
  68. package/templates/react/add-on/store/assets/src/lib/demo-store.ts +13 -0
  69. package/templates/react/add-on/store/assets/src/routes/demo.store.tsx.ejs +75 -0
  70. package/templates/react/add-on/store/info.json +13 -0
  71. package/templates/react/add-on/store/package.json +6 -0
  72. package/templates/react/add-on/tanstack-query/assets/src/integrations/tanstack-query/layout.tsx +5 -0
  73. package/templates/react/add-on/tanstack-query/assets/src/integrations/tanstack-query/provider.tsx +9 -0
  74. package/templates/react/add-on/tanstack-query/assets/src/routes/demo.tanstack-query.tsx.ejs +38 -0
  75. package/templates/react/add-on/tanstack-query/info.json +13 -0
  76. package/templates/react/add-on/tanstack-query/package.json +6 -0
  77. package/templates/{base → react/base}/README.md.ejs +10 -1
  78. package/templates/react/base/src/components/Header.tsx.ejs +25 -0
  79. package/templates/{base/tsconfig.json → react/base/tsconfig.json.ejs} +5 -1
  80. package/templates/react/base/vite.config.js.ejs +24 -0
  81. package/templates/{code-router → react/code-router}/src/main.tsx.ejs +18 -1
  82. package/templates/react/example/tanchat/README.md +37 -0
  83. package/templates/react/example/tanchat/assets/_dot_env.local.append +2 -0
  84. package/templates/react/example/tanchat/assets/src/components/demo.SettingsDialog.tsx +148 -0
  85. package/templates/react/example/tanchat/assets/src/demo.index.css +220 -0
  86. package/templates/react/example/tanchat/assets/src/routes/example.chat.tsx.ejs +375 -0
  87. package/templates/react/example/tanchat/assets/src/store/demo.hooks.ts +21 -0
  88. package/templates/react/example/tanchat/assets/src/store/demo.store.ts +133 -0
  89. package/templates/react/example/tanchat/assets/src/utils/demo.ai.ts +108 -0
  90. package/templates/react/example/tanchat/info.json +15 -0
  91. package/templates/react/example/tanchat/package.json +10 -0
  92. package/templates/{file-router → react/file-router}/src/main.tsx.ejs +1 -1
  93. package/templates/react/file-router/src/routes/__root.tsx.ejs +71 -0
  94. package/templates/solid/add-on/form/assets/src/routes/demo.form.tsx.ejs +148 -0
  95. package/templates/solid/add-on/form/info.json +13 -0
  96. package/templates/solid/add-on/form/package.json +5 -0
  97. package/templates/solid/add-on/module-federation/assets/module-federation.config.js.ejs +27 -0
  98. package/templates/solid/add-on/module-federation/assets/src/demo-mf-component.tsx +3 -0
  99. package/templates/solid/add-on/module-federation/assets/src/demo-mf-self-contained.tsx +9 -0
  100. package/templates/solid/add-on/module-federation/info.json +7 -0
  101. package/templates/solid/add-on/module-federation/package.json +5 -0
  102. package/templates/solid/add-on/sentry/assets/_dot_cursorrules.append +22 -0
  103. package/templates/solid/add-on/sentry/assets/_dot_env.local.append +2 -0
  104. package/templates/solid/add-on/sentry/assets/src/routes/demo.sentry.bad-event-handler.tsx +20 -0
  105. package/templates/solid/add-on/sentry/info.json +13 -0
  106. package/templates/solid/add-on/sentry/package.json +5 -0
  107. package/templates/solid/add-on/solid-ui/README.md +9 -0
  108. package/templates/solid/add-on/solid-ui/assets/src/lib/utils.ts +6 -0
  109. package/templates/solid/add-on/solid-ui/assets/src/styles.css +138 -0
  110. package/templates/solid/add-on/solid-ui/assets/ui.config.json +13 -0
  111. package/templates/solid/add-on/solid-ui/info.json +11 -0
  112. package/templates/solid/add-on/solid-ui/package.json +9 -0
  113. package/templates/solid/add-on/store/assets/src/lib/demo-store.ts +13 -0
  114. package/templates/solid/add-on/store/assets/src/routes/demo.store.tsx.ejs +77 -0
  115. package/templates/solid/add-on/store/info.json +13 -0
  116. package/templates/solid/add-on/store/package.json +6 -0
  117. package/templates/solid/add-on/tanstack-query/assets/src/integrations/tanstack-query/header-user.tsx +5 -0
  118. package/templates/solid/add-on/tanstack-query/assets/src/integrations/tanstack-query/provider.tsx +15 -0
  119. package/templates/solid/add-on/tanstack-query/assets/src/routes/demo.tanstack-query.tsx +30 -0
  120. package/templates/solid/add-on/tanstack-query/info.json +13 -0
  121. package/templates/solid/add-on/tanstack-query/package.json +6 -0
  122. package/templates/solid/base/README.md.ejs +200 -0
  123. package/templates/solid/base/_dot_cursorrules.append +35 -0
  124. package/templates/solid/base/_dot_gitignore +5 -0
  125. package/templates/solid/base/_dot_vscode/settings.json +11 -0
  126. package/templates/solid/base/index.html.ejs +20 -0
  127. package/templates/solid/base/package.json +22 -0
  128. package/templates/solid/base/package.ts.json +5 -0
  129. package/templates/solid/base/package.tw.json +6 -0
  130. package/templates/solid/base/public/favicon.ico +0 -0
  131. package/templates/solid/base/public/logo192.png +0 -0
  132. package/templates/solid/base/public/logo512.png +0 -0
  133. package/templates/solid/base/public/manifest.json +25 -0
  134. package/templates/solid/base/public/robots.txt +3 -0
  135. package/templates/solid/base/src/App.css +0 -0
  136. package/templates/solid/base/src/App.tsx.ejs +47 -0
  137. package/templates/solid/base/src/components/Header.tsx.ejs +26 -0
  138. package/templates/solid/base/src/logo.svg +120 -0
  139. package/templates/solid/base/src/styles.css.ejs +15 -0
  140. package/templates/solid/base/tsconfig.json.ejs +30 -0
  141. package/templates/solid/base/vite.config.js.ejs +22 -0
  142. package/templates/solid/code-router/src/main.tsx.ejs +69 -0
  143. package/templates/solid/file-router/package.fr.json +5 -0
  144. package/templates/solid/file-router/src/main.tsx.ejs +44 -0
  145. package/templates/solid/file-router/src/routes/__root.tsx.ejs +41 -0
  146. package/templates/solid/file-router/src/routes/index.tsx +43 -0
  147. package/templates/base/vite.config.js.ejs +0 -15
  148. package/templates/file-router/src/routes/__root.tsx +0 -11
  149. /package/templates/{base → react/base}/_dot_gitignore +0 -0
  150. /package/templates/{base → react/base}/_dot_vscode/settings.json +0 -0
  151. /package/templates/{base → react/base}/index.html.ejs +0 -0
  152. /package/templates/{base → react/base}/package.json +0 -0
  153. /package/templates/{base → react/base}/package.ts.json +0 -0
  154. /package/templates/{base → react/base}/package.tw.json +0 -0
  155. /package/templates/{base → react/base}/public/favicon.ico +0 -0
  156. /package/templates/{base → react/base}/public/logo192.png +0 -0
  157. /package/templates/{base → react/base}/public/logo512.png +0 -0
  158. /package/templates/{base → react/base}/public/manifest.json +0 -0
  159. /package/templates/{base → react/base}/public/robots.txt +0 -0
  160. /package/templates/{base → react/base}/src/App.css +0 -0
  161. /package/templates/{base → react/base}/src/App.test.tsx.ejs +0 -0
  162. /package/templates/{base → react/base}/src/App.tsx.ejs +0 -0
  163. /package/templates/{base → react/base}/src/logo.svg +0 -0
  164. /package/templates/{base → react/base}/src/reportWebVitals.ts.ejs +0 -0
  165. /package/templates/{base → react/base}/src/styles.css.ejs +0 -0
  166. /package/templates/{file-router → react/file-router}/package.fr.json +0 -0
@@ -0,0 +1,261 @@
1
+ import { cancel, confirm, isCancel, multiselect, select, text, } from '@clack/prompts';
2
+ import { DEFAULT_PACKAGE_MANAGER, SUPPORTED_PACKAGE_MANAGERS, getPackageManager, } from './package-manager.js';
3
+ import { CODE_ROUTER, DEFAULT_FRAMEWORK, FILE_ROUTER } from './constants.js';
4
+ import { finalizeAddOns, getAllAddOns } from './add-ons.js';
5
+ // If all CLI options are provided, use them directly
6
+ export async function normalizeOptions(cliOptions) {
7
+ if (cliOptions.projectName) {
8
+ let typescript = cliOptions.template === 'typescript' ||
9
+ cliOptions.template === 'file-router' ||
10
+ cliOptions.framework === 'solid';
11
+ let tailwind = !!cliOptions.tailwind;
12
+ if (cliOptions.framework === 'solid') {
13
+ tailwind = true;
14
+ }
15
+ let addOns = false;
16
+ let chosenAddOns = [];
17
+ if (Array.isArray(cliOptions.addOns)) {
18
+ addOns = true;
19
+ chosenAddOns = await finalizeAddOns(cliOptions.framework || DEFAULT_FRAMEWORK, cliOptions.template === 'file-router' ? FILE_ROUTER : CODE_ROUTER, cliOptions.addOns);
20
+ tailwind = true;
21
+ typescript = true;
22
+ }
23
+ return {
24
+ framework: cliOptions.framework || 'react',
25
+ projectName: cliOptions.projectName,
26
+ typescript,
27
+ tailwind,
28
+ packageManager: cliOptions.packageManager || DEFAULT_PACKAGE_MANAGER,
29
+ mode: cliOptions.template === 'file-router' ? FILE_ROUTER : CODE_ROUTER,
30
+ git: !!cliOptions.git,
31
+ addOns,
32
+ chosenAddOns,
33
+ variableValues: {},
34
+ };
35
+ }
36
+ }
37
+ async function collectVariables(variables) {
38
+ const responses = {};
39
+ for (const variable of variables) {
40
+ if (variable.type === 'string') {
41
+ const response = await text({
42
+ message: variable.description,
43
+ initialValue: variable.default,
44
+ });
45
+ if (isCancel(response)) {
46
+ cancel('Operation cancelled.');
47
+ process.exit(0);
48
+ }
49
+ responses[variable.name] = response;
50
+ }
51
+ else if (variable.type === 'number') {
52
+ const response = await text({
53
+ message: variable.description,
54
+ initialValue: variable.default.toString(),
55
+ });
56
+ if (isCancel(response)) {
57
+ cancel('Operation cancelled.');
58
+ process.exit(0);
59
+ }
60
+ responses[variable.name] = Number(response);
61
+ }
62
+ else {
63
+ const response = await confirm({
64
+ message: variable.description,
65
+ initialValue: variable.default === true,
66
+ });
67
+ if (isCancel(response)) {
68
+ cancel('Operation cancelled.');
69
+ process.exit(0);
70
+ }
71
+ responses[variable.name] = response;
72
+ }
73
+ }
74
+ return responses;
75
+ }
76
+ export async function promptForOptions(cliOptions) {
77
+ const options = {};
78
+ options.framework = cliOptions.framework || DEFAULT_FRAMEWORK;
79
+ if (options.framework === 'solid') {
80
+ options.typescript = true;
81
+ options.tailwind = true;
82
+ }
83
+ if (cliOptions.addOns) {
84
+ options.typescript = true;
85
+ }
86
+ if (!cliOptions.projectName) {
87
+ const value = await text({
88
+ message: 'What would you like to name your project?',
89
+ defaultValue: 'my-app',
90
+ validate(value) {
91
+ if (!value) {
92
+ return 'Please enter a name';
93
+ }
94
+ },
95
+ });
96
+ if (isCancel(value)) {
97
+ cancel('Operation cancelled.');
98
+ process.exit(0);
99
+ }
100
+ options.projectName = value;
101
+ }
102
+ // Router type selection
103
+ if (!cliOptions.template) {
104
+ const routerType = await select({
105
+ message: 'Select the router type:',
106
+ options: [
107
+ {
108
+ value: FILE_ROUTER,
109
+ label: 'File Router - File-based routing structure',
110
+ },
111
+ {
112
+ value: CODE_ROUTER,
113
+ label: 'Code Router - Traditional code-based routing',
114
+ },
115
+ ],
116
+ initialValue: FILE_ROUTER,
117
+ });
118
+ if (isCancel(routerType)) {
119
+ cancel('Operation cancelled.');
120
+ process.exit(0);
121
+ }
122
+ options.mode = routerType;
123
+ }
124
+ else {
125
+ options.mode = cliOptions.template;
126
+ if (options.mode === FILE_ROUTER) {
127
+ options.typescript = true;
128
+ }
129
+ }
130
+ // TypeScript selection (if using Code Router)
131
+ if (!options.typescript) {
132
+ if (options.mode === CODE_ROUTER) {
133
+ const typescriptEnable = await confirm({
134
+ message: 'Would you like to use TypeScript?',
135
+ initialValue: true,
136
+ });
137
+ if (isCancel(typescriptEnable)) {
138
+ cancel('Operation cancelled.');
139
+ process.exit(0);
140
+ }
141
+ options.typescript = typescriptEnable;
142
+ }
143
+ else {
144
+ options.typescript = true;
145
+ }
146
+ }
147
+ // Tailwind selection
148
+ if (cliOptions.tailwind === undefined && options.framework === 'react') {
149
+ const tailwind = await confirm({
150
+ message: 'Would you like to use Tailwind CSS?',
151
+ initialValue: true,
152
+ });
153
+ if (isCancel(tailwind)) {
154
+ cancel('Operation cancelled.');
155
+ process.exit(0);
156
+ }
157
+ options.tailwind = tailwind;
158
+ }
159
+ else {
160
+ options.tailwind = options.framework === 'solid' || !!cliOptions.tailwind;
161
+ }
162
+ // Package manager selection
163
+ if (cliOptions.packageManager === undefined) {
164
+ const detectedPackageManager = getPackageManager();
165
+ if (!detectedPackageManager) {
166
+ const pm = await select({
167
+ message: 'Select package manager:',
168
+ options: SUPPORTED_PACKAGE_MANAGERS.map((pm) => ({
169
+ value: pm,
170
+ label: pm,
171
+ })),
172
+ initialValue: DEFAULT_PACKAGE_MANAGER,
173
+ });
174
+ if (isCancel(pm)) {
175
+ cancel('Operation cancelled.');
176
+ process.exit(0);
177
+ }
178
+ options.packageManager = pm;
179
+ }
180
+ else {
181
+ options.packageManager = detectedPackageManager;
182
+ }
183
+ }
184
+ else {
185
+ options.packageManager = cliOptions.packageManager;
186
+ }
187
+ options.chosenAddOns = [];
188
+ if (Array.isArray(cliOptions.addOns)) {
189
+ options.chosenAddOns = await finalizeAddOns(options.framework, options.mode, cliOptions.addOns);
190
+ options.tailwind = true;
191
+ }
192
+ else if (cliOptions.addOns) {
193
+ // Select any add-ons
194
+ const allAddOns = await getAllAddOns(options.framework, options.mode);
195
+ const addOns = allAddOns.filter((addOn) => addOn.type === 'add-on');
196
+ let selectedAddOns = [];
197
+ if (options.typescript && addOns.length > 0) {
198
+ const value = await multiselect({
199
+ message: 'What add-ons would you like for your project:',
200
+ options: addOns.map((addOn) => ({
201
+ value: addOn.id,
202
+ label: addOn.name,
203
+ hint: addOn.description,
204
+ })),
205
+ required: false,
206
+ });
207
+ if (isCancel(value)) {
208
+ cancel('Operation cancelled.');
209
+ process.exit(0);
210
+ }
211
+ selectedAddOns = value;
212
+ }
213
+ // Select any examples
214
+ const selectedExamples = [];
215
+ // const examples = allAddOns.filter((addOn) => addOn.type === 'example')
216
+ // if (options.typescript && examples.length > 0) {
217
+ // const value = await multiselect({
218
+ // message: 'Would you like any examples?',
219
+ // options: examples.map((addOn) => ({
220
+ // value: addOn.id,
221
+ // label: addOn.name,
222
+ // hint: addOn.description,
223
+ // })),
224
+ // required: false,
225
+ // })
226
+ // if (isCancel(value)) {
227
+ // cancel('Operation cancelled.')
228
+ // process.exit(0)
229
+ // }
230
+ // selectedExamples = value
231
+ // }
232
+ if (selectedAddOns.length > 0 || selectedExamples.length > 0) {
233
+ options.chosenAddOns = await finalizeAddOns(options.framework, options.mode, [...selectedAddOns, ...selectedExamples]);
234
+ options.tailwind = true;
235
+ }
236
+ }
237
+ // Collect variables
238
+ const variables = [];
239
+ for (const addOn of options.chosenAddOns) {
240
+ for (const variable of addOn.variables ?? []) {
241
+ variables.push(variable);
242
+ }
243
+ }
244
+ options.variableValues = await collectVariables(variables);
245
+ // Git selection
246
+ if (cliOptions.git === undefined) {
247
+ const git = await confirm({
248
+ message: 'Would you like to initialize a new git repository?',
249
+ initialValue: true,
250
+ });
251
+ if (isCancel(git)) {
252
+ cancel('Operation cancelled.');
253
+ process.exit(0);
254
+ }
255
+ options.git = git;
256
+ }
257
+ else {
258
+ options.git = !!cliOptions.git;
259
+ }
260
+ return options;
261
+ }
@@ -3,6 +3,7 @@ export const SUPPORTED_PACKAGE_MANAGERS = [
3
3
  'yarn',
4
4
  'pnpm',
5
5
  'bun',
6
+ 'deno',
6
7
  ];
7
8
  export const DEFAULT_PACKAGE_MANAGER = 'npm';
8
9
  export function getPackageManager() {
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-start-app",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Tanstack Application Builder",
5
5
  "bin": "./dist/index.js",
6
6
  "type": "module",
@@ -9,7 +9,8 @@
9
9
  "start": "tsc && node dist/index.js",
10
10
  "test": "npm run test:lint",
11
11
  "cipublish": "node scripts/publish.js",
12
- "test:lint": "eslint ./src"
12
+ "test:lint": "eslint ./src",
13
+ "mcp": "tsc && npx @modelcontextprotocol/inspector dist/index.js --mcp"
13
14
  },
14
15
  "repository": {
15
16
  "type": "git",
@@ -31,9 +32,13 @@
31
32
  "packageManager": "pnpm@9.15.5",
32
33
  "dependencies": {
33
34
  "@clack/prompts": "^0.10.0",
35
+ "@modelcontextprotocol/sdk": "^1.6.0",
36
+ "chalk": "^5.4.1",
34
37
  "commander": "^13.1.0",
35
38
  "ejs": "^3.1.10",
36
- "execa": "^9.5.2"
39
+ "execa": "^9.5.2",
40
+ "prettier": "^3.5.0",
41
+ "zod": "^3.24.2"
37
42
  },
38
43
  "devDependencies": {
39
44
  "@tanstack/config": "^0.16.2",
@@ -42,7 +47,6 @@
42
47
  "eslint": "^9.20.0",
43
48
  "eslint-plugin-react-hooks": "^5.1.0",
44
49
  "eslint-plugin-unused-imports": "^4.1.4",
45
- "prettier": "^3.5.0",
46
50
  "typescript": "^5.6.3"
47
51
  }
48
52
  }
package/src/add-ons.ts ADDED
@@ -0,0 +1,156 @@
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
+ import chalk from 'chalk'
6
+
7
+ import { DEFAULT_FRAMEWORK } from './constants.js'
8
+ import type { CliOptions, Framework } from './types.js'
9
+
10
+ type BooleanVariable = {
11
+ name: string
12
+ default: boolean
13
+ description: string
14
+ type: 'boolean'
15
+ }
16
+
17
+ type NumberVariable = {
18
+ name: string
19
+ default: number
20
+ description: string
21
+ type: 'number'
22
+ }
23
+
24
+ type StringVariable = {
25
+ name: string
26
+ default: string
27
+ description: string
28
+ type: 'string'
29
+ }
30
+
31
+ export type Variable = BooleanVariable | NumberVariable | StringVariable
32
+
33
+ export type AddOn = {
34
+ id: string
35
+ type: 'add-on' | 'example'
36
+ name: string
37
+ description: string
38
+ link: string
39
+ templates: Array<string>
40
+ routes: Array<{
41
+ url: string
42
+ name: string
43
+ }>
44
+ directory: string
45
+ packageAdditions: {
46
+ dependencies?: Record<string, string>
47
+ devDependencies?: Record<string, string>
48
+ scripts?: Record<string, string>
49
+ }
50
+ command?: {
51
+ command: string
52
+ args?: Array<string>
53
+ }
54
+ readme?: string
55
+ phase: 'setup' | 'add-on'
56
+ shadcnComponents?: Array<string>
57
+ warning?: string
58
+ dependsOn?: Array<string>
59
+ variables?: Array<Variable>
60
+ }
61
+
62
+ function isDirectory(path: string): boolean {
63
+ return statSync(path).isDirectory()
64
+ }
65
+
66
+ export async function getAllAddOns(
67
+ framework: Framework,
68
+ template: string,
69
+ ): Promise<Array<AddOn>> {
70
+ const addOns: Array<AddOn> = []
71
+
72
+ for (const type of ['add-on', 'example']) {
73
+ const addOnsBase = fileURLToPath(
74
+ new URL(`../templates/${framework}/${type}`, import.meta.url),
75
+ )
76
+
77
+ if (!existsSync(addOnsBase)) {
78
+ continue
79
+ }
80
+
81
+ for (const dir of await readdirSync(addOnsBase).filter((file) =>
82
+ isDirectory(resolve(addOnsBase, file)),
83
+ )) {
84
+ const filePath = resolve(addOnsBase, dir, 'info.json')
85
+ const fileContent = await readFile(filePath, 'utf-8')
86
+ const info = JSON.parse(fileContent)
87
+
88
+ if (!info.templates.includes(template)) {
89
+ continue
90
+ }
91
+
92
+ let packageAdditions: Record<string, string> = {}
93
+ if (existsSync(resolve(addOnsBase, dir, 'package.json'))) {
94
+ packageAdditions = JSON.parse(
95
+ await readFile(resolve(addOnsBase, dir, 'package.json'), 'utf-8'),
96
+ )
97
+ }
98
+
99
+ let readme: string | undefined
100
+ if (existsSync(resolve(addOnsBase, dir, 'README.md'))) {
101
+ readme = await readFile(resolve(addOnsBase, dir, 'README.md'), 'utf-8')
102
+ }
103
+
104
+ addOns.push({
105
+ ...info,
106
+ id: dir,
107
+ type,
108
+ directory: resolve(addOnsBase, dir),
109
+ packageAdditions,
110
+ readme,
111
+ })
112
+ }
113
+ }
114
+
115
+ return addOns
116
+ }
117
+
118
+ // Turn the list of chosen add-on IDs into a final list of add-ons by resolving dependencies
119
+ export async function finalizeAddOns(
120
+ framework: Framework,
121
+ template: string,
122
+ chosenAddOnIDs: Array<string>,
123
+ ): Promise<Array<AddOn>> {
124
+ const finalAddOnIDs = new Set(chosenAddOnIDs)
125
+
126
+ const addOns = await getAllAddOns(framework, template)
127
+
128
+ for (const addOnID of finalAddOnIDs) {
129
+ const addOn = addOns.find((a) => a.id === addOnID)
130
+ if (!addOn) {
131
+ throw new Error(`Add-on ${addOnID} not found`)
132
+ }
133
+
134
+ for (const dependsOn of addOn.dependsOn || []) {
135
+ const dep = addOns.find((a) => a.id === dependsOn)
136
+ if (!dep) {
137
+ throw new Error(`Dependency ${dependsOn} not found`)
138
+ }
139
+ finalAddOnIDs.add(dep.id)
140
+ }
141
+ }
142
+
143
+ return [...finalAddOnIDs].map((id) => addOns.find((a) => a.id === id)!)
144
+ }
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
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,114 @@
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 runServer from './mcp.js'
9
+ import { listAddOns } from './add-ons.js'
10
+ import { DEFAULT_FRAMEWORK, SUPPORTED_FRAMEWORKS } from './constants.js'
11
+
12
+ import type { PackageManager } from './package-manager.js'
13
+ import type { CliOptions, Framework } from './types.js'
14
+
15
+ export function cli() {
16
+ const program = new Command()
17
+
18
+ program
19
+ .name('create-tsrouter-app')
20
+ .description('CLI to create a new TanStack application')
21
+ .argument('[project-name]', 'name of the project')
22
+ .option('--no-git', 'do not create a git repository')
23
+ .option<Framework>(
24
+ '--framework <type>',
25
+ 'project framework (solid, react)',
26
+ (value) => {
27
+ if (!SUPPORTED_FRAMEWORKS.includes(value as Framework)) {
28
+ throw new InvalidArgumentError(
29
+ `Invalid framework: ${value}. Only the following are allowed: ${SUPPORTED_FRAMEWORKS.join(
30
+ ', ',
31
+ )}`,
32
+ )
33
+ }
34
+ return value as Framework
35
+ },
36
+ DEFAULT_FRAMEWORK,
37
+ )
38
+ .option<'typescript' | 'javascript' | 'file-router'>(
39
+ '--template <type>',
40
+ 'project template (typescript, javascript, file-router)',
41
+ (value) => {
42
+ if (
43
+ value !== 'typescript' &&
44
+ value !== 'javascript' &&
45
+ value !== 'file-router'
46
+ ) {
47
+ throw new InvalidArgumentError(
48
+ `Invalid template: ${value}. Only the following are allowed: typescript, javascript, file-router`,
49
+ )
50
+ }
51
+ return value
52
+ },
53
+ )
54
+ .option<PackageManager>(
55
+ `--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`,
56
+ `Explicitly tell the CLI to use this package manager`,
57
+ (value) => {
58
+ if (!SUPPORTED_PACKAGE_MANAGERS.includes(value as PackageManager)) {
59
+ throw new InvalidArgumentError(
60
+ `Invalid package manager: ${value}. The following are allowed: ${SUPPORTED_PACKAGE_MANAGERS.join(
61
+ ', ',
62
+ )}`,
63
+ )
64
+ }
65
+ return value as PackageManager
66
+ },
67
+ )
68
+ .option('--tailwind', 'add Tailwind CSS', false)
69
+ .option<Array<string> | boolean>(
70
+ '--add-ons [...add-ons]',
71
+ 'pick from a list of available add-ons (comma separated list)',
72
+ (value: string) => {
73
+ let addOns: Array<string> | boolean = !!value
74
+ if (typeof value === 'string') {
75
+ addOns = value.split(',').map((addon) => addon.trim())
76
+ }
77
+ return addOns
78
+ },
79
+ )
80
+ .option('--list-add-ons', 'list all available add-ons', false)
81
+ .option('--mcp', 'run the MCP server', false)
82
+ .action(async (projectName: string, options: CliOptions) => {
83
+ if (options.listAddOns) {
84
+ await listAddOns(options)
85
+ } else if (options.mcp) {
86
+ await runServer()
87
+ } else {
88
+ try {
89
+ const cliOptions = {
90
+ projectName,
91
+ ...options,
92
+ } as CliOptions
93
+
94
+ let finalOptions = await normalizeOptions(cliOptions)
95
+ if (finalOptions) {
96
+ intro(`Creating a new TanStack app in ${projectName}...`)
97
+ } else {
98
+ intro("Let's configure your TanStack application")
99
+ finalOptions = await promptForOptions(cliOptions)
100
+ }
101
+ await createApp(finalOptions)
102
+ } catch (error) {
103
+ log.error(
104
+ error instanceof Error
105
+ ? error.message
106
+ : 'An unknown error occurred',
107
+ )
108
+ process.exit(1)
109
+ }
110
+ }
111
+ })
112
+
113
+ program.parse()
114
+ }
@@ -0,0 +1,7 @@
1
+ import type { Framework } from './types.js'
2
+
3
+ export const CODE_ROUTER = 'code-router'
4
+ export const FILE_ROUTER = 'file-router'
5
+
6
+ export const SUPPORTED_FRAMEWORKS: Array<Framework> = ['solid', 'react']
7
+ export const DEFAULT_FRAMEWORK: Framework = 'react'