create-start-app 0.3.0 → 0.4.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.
Files changed (165) hide show
  1. package/README.md +41 -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 +260 -0
  9. package/dist/{utils/getPackageManager.js → package-manager.js} +1 -0
  10. package/dist/types.js +1 -0
  11. package/package.json +8 -4
  12. package/src/add-ons.ts +156 -0
  13. package/src/cli.ts +114 -0
  14. package/src/constants.ts +7 -0
  15. package/src/create-app.ts +582 -0
  16. package/src/index.ts +2 -507
  17. package/src/mcp.ts +205 -0
  18. package/src/options.ts +308 -0
  19. package/src/{utils/getPackageManager.ts → package-manager.ts} +1 -0
  20. package/src/types.ts +30 -0
  21. package/templates/react/add-on/clerk/README.md +3 -0
  22. package/templates/react/add-on/clerk/assets/_dot_env.local.append +2 -0
  23. package/templates/react/add-on/clerk/assets/src/integrations/clerk/header-user.tsx +19 -0
  24. package/templates/react/add-on/clerk/assets/src/integrations/clerk/provider.tsx +18 -0
  25. package/templates/react/add-on/clerk/assets/src/routes/demo.clerk.tsx +20 -0
  26. package/templates/react/add-on/clerk/info.json +13 -0
  27. package/templates/react/add-on/clerk/package.json +5 -0
  28. package/templates/react/add-on/convex/README.md +4 -0
  29. package/templates/react/add-on/convex/assets/_dot_cursorrules.append +93 -0
  30. package/templates/react/add-on/convex/assets/_dot_env.local.append +3 -0
  31. package/templates/react/add-on/convex/assets/convex/products.ts +8 -0
  32. package/templates/react/add-on/convex/assets/convex/schema.ts +10 -0
  33. package/templates/react/add-on/convex/assets/src/integrations/convex/provider.tsx +20 -0
  34. package/templates/react/add-on/convex/assets/src/routes/demo.convex.tsx +33 -0
  35. package/templates/react/add-on/convex/info.json +13 -0
  36. package/templates/react/add-on/convex/package.json +6 -0
  37. package/templates/react/add-on/form/assets/src/routes/demo.form.tsx.ejs +62 -0
  38. package/templates/react/add-on/form/info.json +13 -0
  39. package/templates/react/add-on/form/package.json +5 -0
  40. package/templates/react/add-on/module-federation/assets/module-federation.config.js.ejs +31 -0
  41. package/templates/react/add-on/module-federation/assets/src/demo-mf-component.tsx +3 -0
  42. package/templates/react/add-on/module-federation/assets/src/demo-mf-self-contained.tsx +11 -0
  43. package/templates/react/add-on/module-federation/info.json +7 -0
  44. package/templates/react/add-on/module-federation/package.json +5 -0
  45. package/templates/react/add-on/netlify/README.md +11 -0
  46. package/templates/react/add-on/netlify/info.json +7 -0
  47. package/templates/react/add-on/sentry/assets/_dot_cursorrules.append +22 -0
  48. package/templates/react/add-on/sentry/assets/_dot_env.local.append +2 -0
  49. package/templates/react/add-on/sentry/assets/src/app/global-middleware.ts +25 -0
  50. package/templates/react/add-on/sentry/assets/src/routes/demo.sentry.testing.tsx +480 -0
  51. package/templates/react/add-on/sentry/info.json +14 -0
  52. package/templates/react/add-on/sentry/package.json +7 -0
  53. package/templates/react/add-on/shadcn/README.md +7 -0
  54. package/templates/react/add-on/shadcn/assets/_dot_cursorrules.append +7 -0
  55. package/templates/react/add-on/shadcn/info.json +11 -0
  56. package/templates/react/add-on/start/assets/app.config.ts +16 -0
  57. package/templates/react/add-on/start/assets/postcss.config.ts +5 -0
  58. package/templates/react/add-on/start/assets/src/api.ts +6 -0
  59. package/templates/react/add-on/start/assets/src/client.tsx +10 -0
  60. package/templates/react/add-on/start/assets/src/router.tsx.ejs +51 -0
  61. package/templates/react/add-on/start/assets/src/routes/api.demo-names.ts +11 -0
  62. package/templates/react/add-on/start/assets/src/routes/demo.start.api-request.tsx.ejs +33 -0
  63. package/templates/react/add-on/start/assets/src/routes/demo.start.server-funcs.tsx +49 -0
  64. package/templates/react/add-on/start/assets/src/ssr.tsx +12 -0
  65. package/templates/react/add-on/start/info.json +19 -0
  66. package/templates/react/add-on/start/package.json +14 -0
  67. package/templates/react/add-on/store/assets/src/lib/demo-store.ts +13 -0
  68. package/templates/react/add-on/store/assets/src/routes/demo.store.tsx.ejs +75 -0
  69. package/templates/react/add-on/store/info.json +13 -0
  70. package/templates/react/add-on/store/package.json +6 -0
  71. package/templates/react/add-on/tanstack-query/assets/src/integrations/tanstack-query/layout.tsx +5 -0
  72. package/templates/react/add-on/tanstack-query/assets/src/integrations/tanstack-query/provider.tsx +9 -0
  73. package/templates/react/add-on/tanstack-query/assets/src/routes/demo.tanstack-query.tsx.ejs +38 -0
  74. package/templates/react/add-on/tanstack-query/info.json +13 -0
  75. package/templates/react/add-on/tanstack-query/package.json +6 -0
  76. package/templates/{base → react/base}/README.md.ejs +9 -0
  77. package/templates/react/base/_dot_vscode/settings.json +11 -0
  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 +17 -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/react/file-router/src/routes/__root.tsx.ejs +71 -0
  93. package/templates/solid/add-on/form/assets/src/routes/demo.form.tsx.ejs +148 -0
  94. package/templates/solid/add-on/form/info.json +13 -0
  95. package/templates/solid/add-on/form/package.json +5 -0
  96. package/templates/solid/add-on/module-federation/assets/module-federation.config.js.ejs +27 -0
  97. package/templates/solid/add-on/module-federation/assets/src/demo-mf-component.tsx +3 -0
  98. package/templates/solid/add-on/module-federation/assets/src/demo-mf-self-contained.tsx +9 -0
  99. package/templates/solid/add-on/module-federation/info.json +7 -0
  100. package/templates/solid/add-on/module-federation/package.json +5 -0
  101. package/templates/solid/add-on/sentry/assets/_dot_cursorrules.append +22 -0
  102. package/templates/solid/add-on/sentry/assets/_dot_env.local.append +2 -0
  103. package/templates/solid/add-on/sentry/assets/src/routes/demo.sentry.bad-event-handler.tsx +20 -0
  104. package/templates/solid/add-on/sentry/info.json +13 -0
  105. package/templates/solid/add-on/sentry/package.json +5 -0
  106. package/templates/solid/add-on/solid-ui/README.md +9 -0
  107. package/templates/solid/add-on/solid-ui/assets/src/lib/utils.ts +6 -0
  108. package/templates/solid/add-on/solid-ui/assets/src/styles.css +138 -0
  109. package/templates/solid/add-on/solid-ui/assets/ui.config.json +13 -0
  110. package/templates/solid/add-on/solid-ui/info.json +11 -0
  111. package/templates/solid/add-on/solid-ui/package.json +9 -0
  112. package/templates/solid/add-on/store/assets/src/lib/demo-store.ts +13 -0
  113. package/templates/solid/add-on/store/assets/src/routes/demo.store.tsx.ejs +77 -0
  114. package/templates/solid/add-on/store/info.json +13 -0
  115. package/templates/solid/add-on/store/package.json +6 -0
  116. package/templates/solid/add-on/tanstack-query/assets/src/integrations/tanstack-query/header-user.tsx +5 -0
  117. package/templates/solid/add-on/tanstack-query/assets/src/integrations/tanstack-query/provider.tsx +15 -0
  118. package/templates/solid/add-on/tanstack-query/assets/src/routes/demo.tanstack-query.tsx +30 -0
  119. package/templates/solid/add-on/tanstack-query/info.json +13 -0
  120. package/templates/solid/add-on/tanstack-query/package.json +6 -0
  121. package/templates/solid/base/README.md.ejs +200 -0
  122. package/templates/solid/base/_dot_cursorrules.append +35 -0
  123. package/templates/solid/base/_dot_gitignore +5 -0
  124. package/templates/solid/base/_dot_vscode/settings.json +11 -0
  125. package/templates/solid/base/index.html.ejs +20 -0
  126. package/templates/solid/base/package.json +22 -0
  127. package/templates/solid/base/package.ts.json +5 -0
  128. package/templates/solid/base/package.tw.json +6 -0
  129. package/templates/solid/base/public/favicon.ico +0 -0
  130. package/templates/solid/base/public/logo192.png +0 -0
  131. package/templates/solid/base/public/logo512.png +0 -0
  132. package/templates/solid/base/public/manifest.json +25 -0
  133. package/templates/solid/base/public/robots.txt +3 -0
  134. package/templates/solid/base/src/App.css +0 -0
  135. package/templates/solid/base/src/App.tsx.ejs +47 -0
  136. package/templates/solid/base/src/components/Header.tsx.ejs +26 -0
  137. package/templates/solid/base/src/logo.svg +120 -0
  138. package/templates/solid/base/src/styles.css.ejs +15 -0
  139. package/templates/solid/base/tsconfig.json.ejs +30 -0
  140. package/templates/solid/base/vite.config.js.ejs +22 -0
  141. package/templates/solid/code-router/src/main.tsx.ejs +69 -0
  142. package/templates/solid/file-router/package.fr.json +5 -0
  143. package/templates/solid/file-router/src/main.tsx.ejs +44 -0
  144. package/templates/solid/file-router/src/routes/__root.tsx.ejs +41 -0
  145. package/templates/solid/file-router/src/routes/index.tsx +43 -0
  146. package/templates/base/vite.config.js.ejs +0 -15
  147. package/templates/file-router/src/routes/__root.tsx +0 -11
  148. /package/templates/{base/gitignore → react/base/_dot_gitignore} +0 -0
  149. /package/templates/{base → react/base}/index.html.ejs +0 -0
  150. /package/templates/{base → react/base}/package.json +0 -0
  151. /package/templates/{base → react/base}/package.ts.json +0 -0
  152. /package/templates/{base → react/base}/package.tw.json +0 -0
  153. /package/templates/{base → react/base}/public/favicon.ico +0 -0
  154. /package/templates/{base → react/base}/public/logo192.png +0 -0
  155. /package/templates/{base → react/base}/public/logo512.png +0 -0
  156. /package/templates/{base → react/base}/public/manifest.json +0 -0
  157. /package/templates/{base → react/base}/public/robots.txt +0 -0
  158. /package/templates/{base → react/base}/src/App.css +0 -0
  159. /package/templates/{base → react/base}/src/App.test.tsx.ejs +0 -0
  160. /package/templates/{base → react/base}/src/App.tsx.ejs +0 -0
  161. /package/templates/{base → react/base}/src/logo.svg +0 -0
  162. /package/templates/{base → react/base}/src/reportWebVitals.ts.ejs +0 -0
  163. /package/templates/{base → react/base}/src/styles.css.ejs +0 -0
  164. /package/templates/{file-router → react/file-router}/package.fr.json +0 -0
  165. /package/templates/{file-router → react/file-router}/src/main.tsx.ejs +0 -0
package/README.md CHANGED
@@ -8,13 +8,14 @@ To help accelerate the migration away from `create-react-app` we created the `cr
8
8
 
9
9
  To maintain compatability with `create-react-app` you can build a new application by running:
10
10
 
11
- | Command | Description |
12
- | -------------------------------------------------------------- | --------------------------- |
13
- | `npx create-tsrouter-app@latest my-app` | Create a new app |
14
- | `npx create-tsrouter-app@latest my-app --template file-router` | Create a new file based app |
15
- | `npx create-tsrouter-app@latest my-app --template typescript` | Create a new TypeScript app |
16
- | `npx create-tsrouter-app@latest my-app --template javascript` | Create a new JavaScript app |
17
- | `npx create-tsrouter-app@latest my-app --tailwind` | Add Tailwind CSS support |
11
+ | Command | Description |
12
+ | -------------------------------------------------------------------------------- | ------------------------------------------------- |
13
+ | `npx create-tsrouter-app@latest my-app` | Create a new app |
14
+ | `npx create-tsrouter-app@latest my-app --template file-router` | Create a new file based app |
15
+ | `npx create-tsrouter-app@latest my-app --template typescript` | Create a new TypeScript app using the Code Router |
16
+ | `npx create-tsrouter-app@latest my-app --tailwind` | Add Tailwind CSS support |
17
+ | `npx create-tsrouter-app@latest my-app --framework solid` | Create a Solid app |
18
+ | `npx create-tsrouter-app@latest my-app --framework solid --template file-router` | Create a Solid app with file-router |
18
19
 
19
20
  If you don't specify a project name, the CLI will walk you through an interactive setup process:
20
21
 
@@ -43,8 +44,9 @@ Available options:
43
44
 
44
45
  - `--template <type>`: Choose between `file-router`, `typescript`, or `javascript`
45
46
  - `--tailwind`: Enable Tailwind CSS
46
- - `--package-manager`: Specify your preferred package manager (`npm`, `yarn`, `pnpm`, or `bun`)
47
+ - `--package-manager`: Specify your preferred package manager (`npm`, `yarn`, `pnpm`, `bun`, or `deno`)
47
48
  - `--no-git`: Do not initialize a git repository
49
+ - `--add-ons`: Enable add-on selection or specify add-ons to install
48
50
 
49
51
  When using flags, the CLI will display which options were provided and only prompt for the remaining choices.
50
52
 
@@ -88,10 +90,40 @@ Enable Tailwind CSS either through the interactive CLI or by adding the `--tailw
88
90
 
89
91
  ### Package Manager
90
92
 
91
- Choose your preferred package manager (`npm`, `bun`, `yarn`, or `pnpm`) either through the interactive CLI or using the `--package-manager` flag.
93
+ Choose your preferred package manager (`npm`, `bun`, `yarn`, `pnpm`, or `deno`) either through the interactive CLI or using the `--package-manager` flag.
92
94
 
93
95
  Extensive documentation on using the TanStack Router, migrating to a File Base Routing approach, as well as integrating [@tanstack/react-query](https://tanstack.com/query/latest) and [@tanstack/store](https://tanstack.com/store/latest) can be found in the generated `README.md` for your project.
94
96
 
97
+ ## Add-ons (experimental)
98
+
99
+ You can enable add-on selection:
100
+
101
+ ```bash
102
+ npx create-tsrouter-app@latest --add-ons
103
+ ```
104
+
105
+ This will prompt you to select the add-ons you want to enable during application creation.
106
+
107
+ You can enable specific add-ons directly by adding a comma separated list of add-on names to the `--add-ons` flag. For example:
108
+
109
+ ```bash
110
+ npx create-tsrouter-app@latest my-app --add-ons shadcn,tanstack-query
111
+ ```
112
+
113
+ You can get a list of all available add-ons by running:
114
+
115
+ ```bash
116
+ npx create-tsrouter-app@latest --list-add-ons
117
+ ```
118
+
119
+ This will display a list of all available add-ons for React that are compatible with the Code Router.
120
+
121
+ ```bash
122
+ npx create-tsrouter-app@latest --list-add-ons --framework solid --template file-router
123
+ ```
124
+
125
+ Will get you a list of all available add-ons for Solid that are compatible with the File Router.
126
+
95
127
  # Contributing
96
128
 
97
129
  Check out the [Contributing](CONTRIBUTING.md) guide.
@@ -0,0 +1,69 @@
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
+ import { DEFAULT_FRAMEWORK } from './constants.js';
7
+ function isDirectory(path) {
8
+ return statSync(path).isDirectory();
9
+ }
10
+ export async function getAllAddOns(framework, template) {
11
+ const addOns = [];
12
+ for (const type of ['add-on', 'example']) {
13
+ const addOnsBase = fileURLToPath(new URL(`../templates/${framework}/${type}`, import.meta.url));
14
+ if (!existsSync(addOnsBase)) {
15
+ continue;
16
+ }
17
+ for (const dir of await readdirSync(addOnsBase).filter((file) => isDirectory(resolve(addOnsBase, file)))) {
18
+ const filePath = resolve(addOnsBase, dir, 'info.json');
19
+ const fileContent = await readFile(filePath, 'utf-8');
20
+ const info = JSON.parse(fileContent);
21
+ if (!info.templates.includes(template)) {
22
+ continue;
23
+ }
24
+ let packageAdditions = {};
25
+ if (existsSync(resolve(addOnsBase, dir, 'package.json'))) {
26
+ packageAdditions = JSON.parse(await readFile(resolve(addOnsBase, dir, 'package.json'), 'utf-8'));
27
+ }
28
+ let readme;
29
+ if (existsSync(resolve(addOnsBase, dir, 'README.md'))) {
30
+ readme = await readFile(resolve(addOnsBase, dir, 'README.md'), 'utf-8');
31
+ }
32
+ addOns.push({
33
+ ...info,
34
+ id: dir,
35
+ type,
36
+ directory: resolve(addOnsBase, dir),
37
+ packageAdditions,
38
+ readme,
39
+ });
40
+ }
41
+ }
42
+ return addOns;
43
+ }
44
+ // Turn the list of chosen add-on IDs into a final list of add-ons by resolving dependencies
45
+ export async function finalizeAddOns(framework, template, chosenAddOnIDs) {
46
+ const finalAddOnIDs = new Set(chosenAddOnIDs);
47
+ const addOns = await getAllAddOns(framework, template);
48
+ for (const addOnID of finalAddOnIDs) {
49
+ const addOn = addOns.find((a) => a.id === addOnID);
50
+ if (!addOn) {
51
+ throw new Error(`Add-on ${addOnID} not found`);
52
+ }
53
+ for (const dependsOn of addOn.dependsOn || []) {
54
+ const dep = addOns.find((a) => a.id === dependsOn);
55
+ if (!dep) {
56
+ throw new Error(`Dependency ${dependsOn} not found`);
57
+ }
58
+ finalAddOnIDs.add(dep.id);
59
+ }
60
+ }
61
+ return [...finalAddOnIDs].map((id) => addOns.find((a) => a.id === id));
62
+ }
63
+ export async function listAddOns(options) {
64
+ const mode = options.template === 'file-router' ? 'file-router' : 'code-router';
65
+ const addOns = await getAllAddOns(options.framework || DEFAULT_FRAMEWORK, mode);
66
+ for (const addOn of addOns) {
67
+ console.log(`${chalk.bold(addOn.id)}: ${addOn.description}`);
68
+ }
69
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,78 @@
1
+ import { Command, InvalidArgumentError } from 'commander';
2
+ import { intro, log } from '@clack/prompts';
3
+ import { createApp } from './create-app.js';
4
+ import { normalizeOptions, promptForOptions } from './options.js';
5
+ import { SUPPORTED_PACKAGE_MANAGERS } from './package-manager.js';
6
+ import runServer from './mcp.js';
7
+ import { listAddOns } from './add-ons.js';
8
+ import { DEFAULT_FRAMEWORK, SUPPORTED_FRAMEWORKS } from './constants.js';
9
+ export function cli() {
10
+ const program = new Command();
11
+ program
12
+ .name('create-tsrouter-app')
13
+ .description('CLI to create a new TanStack application')
14
+ .argument('[project-name]', 'name of the project')
15
+ .option('--no-git', 'do not create a git repository')
16
+ .option('--framework <type>', 'project framework (solid, react)', (value) => {
17
+ if (!SUPPORTED_FRAMEWORKS.includes(value)) {
18
+ throw new InvalidArgumentError(`Invalid framework: ${value}. Only the following are allowed: ${SUPPORTED_FRAMEWORKS.join(', ')}`);
19
+ }
20
+ return value;
21
+ }, DEFAULT_FRAMEWORK)
22
+ .option('--template <type>', 'project template (typescript, javascript, file-router)', (value) => {
23
+ if (value !== 'typescript' &&
24
+ value !== 'javascript' &&
25
+ value !== 'file-router') {
26
+ throw new InvalidArgumentError(`Invalid template: ${value}. Only the following are allowed: typescript, javascript, file-router`);
27
+ }
28
+ return value;
29
+ })
30
+ .option(`--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`, `Explicitly tell the CLI to use this package manager`, (value) => {
31
+ if (!SUPPORTED_PACKAGE_MANAGERS.includes(value)) {
32
+ throw new InvalidArgumentError(`Invalid package manager: ${value}. The following are allowed: ${SUPPORTED_PACKAGE_MANAGERS.join(', ')}`);
33
+ }
34
+ return value;
35
+ })
36
+ .option('--tailwind', 'add Tailwind CSS', false)
37
+ .option('--add-ons [...add-ons]', 'pick from a list of available add-ons (comma separated list)', (value) => {
38
+ let addOns = !!value;
39
+ if (typeof value === 'string') {
40
+ addOns = value.split(',').map((addon) => addon.trim());
41
+ }
42
+ return addOns;
43
+ })
44
+ .option('--list-add-ons', 'list all available add-ons', false)
45
+ .option('--mcp', 'run the MCP server', false)
46
+ .action(async (projectName, options) => {
47
+ if (options.listAddOns) {
48
+ await listAddOns(options);
49
+ }
50
+ else if (options.mcp) {
51
+ await runServer();
52
+ }
53
+ else {
54
+ try {
55
+ const cliOptions = {
56
+ projectName,
57
+ ...options,
58
+ };
59
+ let finalOptions = await normalizeOptions(cliOptions);
60
+ if (finalOptions) {
61
+ intro(`Creating a new TanStack app in ${projectName}...`);
62
+ }
63
+ else {
64
+ intro("Let's configure your TanStack application");
65
+ finalOptions = await promptForOptions(cliOptions);
66
+ }
67
+ await createApp(finalOptions);
68
+ }
69
+ catch (error) {
70
+ log.error(error instanceof Error
71
+ ? error.message
72
+ : 'An unknown error occurred');
73
+ process.exit(1);
74
+ }
75
+ }
76
+ });
77
+ program.parse();
78
+ }
@@ -0,0 +1,4 @@
1
+ export const CODE_ROUTER = 'code-router';
2
+ export const FILE_ROUTER = 'file-router';
3
+ export const SUPPORTED_FRAMEWORKS = ['solid', 'react'];
4
+ export const DEFAULT_FRAMEWORK = 'react';
@@ -0,0 +1,371 @@
1
+ #!/usr/bin/env node
2
+ import { appendFile, copyFile, mkdir, readFile, writeFile, } from 'node:fs/promises';
3
+ import { existsSync, readdirSync, statSync } from 'node:fs';
4
+ import { basename, dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { log, outro, spinner } from '@clack/prompts';
7
+ import { execa } from 'execa';
8
+ import { render } from 'ejs';
9
+ import { format } from 'prettier';
10
+ import chalk from 'chalk';
11
+ import { CODE_ROUTER, FILE_ROUTER } from './constants.js';
12
+ function sortObject(obj) {
13
+ return Object.keys(obj)
14
+ .sort()
15
+ .reduce((acc, key) => {
16
+ acc[key] = obj[key];
17
+ return acc;
18
+ }, {});
19
+ }
20
+ function createCopyFiles(targetDir) {
21
+ return async function copyFiles(templateDir, files) {
22
+ for (const file of files) {
23
+ const targetFileName = file.replace('.tw', '');
24
+ await copyFile(resolve(templateDir, file), resolve(targetDir, targetFileName));
25
+ }
26
+ };
27
+ }
28
+ function jsSafeName(name) {
29
+ return name
30
+ .split(/[^a-zA-Z0-9]/)
31
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
32
+ .join('');
33
+ }
34
+ function createTemplateFile(projectName, options, targetDir) {
35
+ return async function templateFile(templateDir, file, targetFileName, extraTemplateValues) {
36
+ const templateValues = {
37
+ packageManager: options.packageManager,
38
+ projectName: projectName,
39
+ typescript: options.typescript,
40
+ tailwind: options.tailwind,
41
+ js: options.typescript ? 'ts' : 'js',
42
+ jsx: options.typescript ? 'tsx' : 'jsx',
43
+ fileRouter: options.mode === FILE_ROUTER,
44
+ codeRouter: options.mode === CODE_ROUTER,
45
+ addOnEnabled: options.chosenAddOns.reduce((acc, addOn) => {
46
+ acc[addOn.id] = true;
47
+ return acc;
48
+ }, {}),
49
+ addOns: options.chosenAddOns,
50
+ ...extraTemplateValues,
51
+ };
52
+ const template = await readFile(resolve(templateDir, file), 'utf-8');
53
+ let content = '';
54
+ try {
55
+ content = render(template, templateValues);
56
+ }
57
+ catch (error) {
58
+ console.error(chalk.red(`EJS error in file ${file}`));
59
+ console.error(error);
60
+ process.exit(1);
61
+ }
62
+ const target = targetFileName ?? file.replace('.ejs', '');
63
+ if (target.endsWith('.ts') || target.endsWith('.tsx')) {
64
+ content = await format(content, {
65
+ semi: false,
66
+ singleQuote: true,
67
+ trailingComma: 'all',
68
+ parser: 'typescript',
69
+ });
70
+ }
71
+ await mkdir(dirname(resolve(targetDir, target)), {
72
+ recursive: true,
73
+ });
74
+ await writeFile(resolve(targetDir, target), content);
75
+ };
76
+ }
77
+ async function createPackageJSON(projectName, options, templateDir, routerDir, targetDir, addOns) {
78
+ let packageJSON = JSON.parse(await readFile(resolve(templateDir, 'package.json'), 'utf8'));
79
+ packageJSON.name = projectName;
80
+ if (options.typescript) {
81
+ const tsPackageJSON = JSON.parse(await readFile(resolve(templateDir, 'package.ts.json'), 'utf8'));
82
+ packageJSON = {
83
+ ...packageJSON,
84
+ devDependencies: {
85
+ ...packageJSON.devDependencies,
86
+ ...tsPackageJSON.devDependencies,
87
+ },
88
+ };
89
+ }
90
+ if (options.tailwind) {
91
+ const twPackageJSON = JSON.parse(await readFile(resolve(templateDir, 'package.tw.json'), 'utf8'));
92
+ packageJSON = {
93
+ ...packageJSON,
94
+ dependencies: {
95
+ ...packageJSON.dependencies,
96
+ ...twPackageJSON.dependencies,
97
+ },
98
+ };
99
+ }
100
+ if (options.mode === FILE_ROUTER) {
101
+ const frPackageJSON = JSON.parse(await readFile(resolve(routerDir, 'package.fr.json'), 'utf8'));
102
+ packageJSON = {
103
+ ...packageJSON,
104
+ dependencies: {
105
+ ...packageJSON.dependencies,
106
+ ...frPackageJSON.dependencies,
107
+ },
108
+ };
109
+ }
110
+ for (const addOn of addOns) {
111
+ packageJSON = {
112
+ ...packageJSON,
113
+ dependencies: {
114
+ ...packageJSON.dependencies,
115
+ ...addOn.dependencies,
116
+ },
117
+ devDependencies: {
118
+ ...packageJSON.devDependencies,
119
+ ...addOn.devDependencies,
120
+ },
121
+ scripts: {
122
+ ...packageJSON.scripts,
123
+ ...addOn.scripts,
124
+ },
125
+ };
126
+ }
127
+ packageJSON.dependencies = sortObject(packageJSON.dependencies);
128
+ packageJSON.devDependencies = sortObject(packageJSON.devDependencies);
129
+ await writeFile(resolve(targetDir, 'package.json'), JSON.stringify(packageJSON, null, 2));
130
+ }
131
+ async function copyFilesRecursively(source, target, copyFile, templateFile) {
132
+ const sourceStat = statSync(source);
133
+ if (sourceStat.isDirectory()) {
134
+ const files = readdirSync(source);
135
+ for (const file of files) {
136
+ const sourceChild = resolve(source, file);
137
+ const targetChild = resolve(target, file);
138
+ await copyFilesRecursively(sourceChild, targetChild, copyFile, templateFile);
139
+ }
140
+ }
141
+ else {
142
+ let targetFile = basename(target).replace(/_dot_/, '.');
143
+ let isTemplate = false;
144
+ if (targetFile.endsWith('.ejs')) {
145
+ targetFile = targetFile.replace('.ejs', '');
146
+ isTemplate = true;
147
+ }
148
+ let isAppend = false;
149
+ if (targetFile.endsWith('.append')) {
150
+ targetFile = targetFile.replace('.append', '');
151
+ isAppend = true;
152
+ }
153
+ const targetPath = resolve(dirname(target), targetFile);
154
+ await mkdir(dirname(targetPath), {
155
+ recursive: true,
156
+ });
157
+ if (isTemplate) {
158
+ await templateFile(source, targetPath);
159
+ }
160
+ else {
161
+ if (isAppend) {
162
+ await appendFile(targetPath, (await readFile(source)).toString());
163
+ }
164
+ else {
165
+ await copyFile(source, targetPath);
166
+ }
167
+ }
168
+ }
169
+ }
170
+ export async function createApp(options, { silent = false, } = {}) {
171
+ const templateDirBase = fileURLToPath(new URL(`../templates/${options.framework}/base`, import.meta.url));
172
+ const templateDirRouter = fileURLToPath(new URL(`../templates/${options.framework}/${options.mode}`, import.meta.url));
173
+ const targetDir = resolve(process.cwd(), options.projectName);
174
+ if (existsSync(targetDir)) {
175
+ if (!silent) {
176
+ log.error(`Directory "${options.projectName}" already exists`);
177
+ }
178
+ return;
179
+ }
180
+ const copyFiles = createCopyFiles(targetDir);
181
+ const templateFile = createTemplateFile(options.projectName, options, targetDir);
182
+ const isAddOnEnabled = (id) => options.chosenAddOns.find((a) => a.id === id);
183
+ // Make the root directory
184
+ await mkdir(targetDir, { recursive: true });
185
+ // Setup the .vscode directory
186
+ await mkdir(resolve(targetDir, '.vscode'), { recursive: true });
187
+ await copyFile(resolve(templateDirBase, '_dot_vscode/settings.json'), resolve(targetDir, '.vscode/settings.json'));
188
+ // Fill the public directory
189
+ await mkdir(resolve(targetDir, 'public'), { recursive: true });
190
+ copyFiles(templateDirBase, [
191
+ './public/robots.txt',
192
+ './public/favicon.ico',
193
+ './public/manifest.json',
194
+ './public/logo192.png',
195
+ './public/logo512.png',
196
+ ]);
197
+ // Make the src directory
198
+ await mkdir(resolve(targetDir, 'src'), { recursive: true });
199
+ if (options.mode === FILE_ROUTER) {
200
+ await mkdir(resolve(targetDir, 'src/routes'), { recursive: true });
201
+ await mkdir(resolve(targetDir, 'src/components'), { recursive: true });
202
+ }
203
+ // Check for a .cursorrules file
204
+ if (existsSync(resolve(templateDirBase, '.cursorrules'))) {
205
+ await copyFile(resolve(templateDirBase, '.cursorrules'), resolve(targetDir, '.cursorrules'));
206
+ }
207
+ // Copy in Vite and Tailwind config and CSS
208
+ if (!options.tailwind) {
209
+ await copyFiles(templateDirBase, ['./src/App.css']);
210
+ }
211
+ await templateFile(templateDirBase, './vite.config.js.ejs');
212
+ await templateFile(templateDirBase, './src/styles.css.ejs');
213
+ copyFiles(templateDirBase, ['./src/logo.svg']);
214
+ // Setup the main, reportWebVitals and index.html files
215
+ if (!isAddOnEnabled('start') && options.framework === 'react') {
216
+ if (options.typescript) {
217
+ await templateFile(templateDirBase, './src/reportWebVitals.ts.ejs');
218
+ }
219
+ else {
220
+ await templateFile(templateDirBase, './src/reportWebVitals.ts.ejs', './src/reportWebVitals.js');
221
+ }
222
+ }
223
+ if (!isAddOnEnabled('start')) {
224
+ await templateFile(templateDirBase, './index.html.ejs');
225
+ }
226
+ // Setup tsconfig
227
+ if (options.typescript) {
228
+ await templateFile(templateDirBase, './tsconfig.json.ejs', './tsconfig.json');
229
+ }
230
+ // Setup the package.json file, optionally with typescript and tailwind
231
+ await createPackageJSON(options.projectName, options, templateDirBase, templateDirRouter, targetDir, options.chosenAddOns.map((addOn) => addOn.packageAdditions));
232
+ // Copy all the asset files from the addons
233
+ const s = silent ? null : spinner();
234
+ for (const phase of ['setup', 'add-on', 'example']) {
235
+ for (const addOn of options.chosenAddOns.filter((addOn) => addOn.phase === phase)) {
236
+ s?.start(`Setting up ${addOn.name}...`);
237
+ const addOnDir = resolve(addOn.directory, 'assets');
238
+ if (existsSync(addOnDir)) {
239
+ await copyFilesRecursively(addOnDir, targetDir, copyFile, async (file, targetFileName) => templateFile(addOnDir, file, targetFileName));
240
+ }
241
+ if (addOn.command) {
242
+ await execa(addOn.command.command, addOn.command.args || [], {
243
+ cwd: targetDir,
244
+ });
245
+ }
246
+ s?.stop(`${addOn.name} setup complete`);
247
+ }
248
+ }
249
+ if (isAddOnEnabled('shadcn')) {
250
+ const shadcnComponents = new Set();
251
+ for (const addOn of options.chosenAddOns) {
252
+ if (addOn.shadcnComponents) {
253
+ for (const component of addOn.shadcnComponents) {
254
+ shadcnComponents.add(component);
255
+ }
256
+ }
257
+ }
258
+ if (shadcnComponents.size > 0) {
259
+ s?.start(`Installing shadcn components (${Array.from(shadcnComponents).join(', ')})...`);
260
+ await execa('npx', ['shadcn@canary', 'add', ...shadcnComponents], {
261
+ cwd: targetDir,
262
+ });
263
+ s?.stop(`Installed shadcn components`);
264
+ }
265
+ }
266
+ const integrations = [];
267
+ if (existsSync(resolve(targetDir, 'src/integrations'))) {
268
+ for (const integration of readdirSync(resolve(targetDir, 'src/integrations'))) {
269
+ const integrationName = jsSafeName(integration);
270
+ if (existsSync(resolve(targetDir, 'src/integrations', integration, 'layout.tsx'))) {
271
+ integrations.push({
272
+ type: 'layout',
273
+ name: `${integrationName}Layout`,
274
+ path: `integrations/${integration}/layout`,
275
+ });
276
+ }
277
+ if (existsSync(resolve(targetDir, 'src/integrations', integration, 'provider.tsx'))) {
278
+ integrations.push({
279
+ type: 'provider',
280
+ name: `${integrationName}Provider`,
281
+ path: `integrations/${integration}/provider`,
282
+ });
283
+ }
284
+ if (existsSync(resolve(targetDir, 'src/integrations', integration, 'header-user.tsx'))) {
285
+ integrations.push({
286
+ type: 'header-user',
287
+ name: `${integrationName}Header`,
288
+ path: `integrations/${integration}/header-user`,
289
+ });
290
+ }
291
+ }
292
+ }
293
+ const routes = [];
294
+ if (existsSync(resolve(targetDir, 'src/routes'))) {
295
+ for (const file of readdirSync(resolve(targetDir, 'src/routes'))) {
296
+ const name = file.replace(/\.tsx?|\.jsx?/, '');
297
+ const safeRouteName = jsSafeName(name);
298
+ routes.push({
299
+ path: `./routes/${name}`,
300
+ name: safeRouteName,
301
+ });
302
+ }
303
+ }
304
+ // Create the main entry point
305
+ if (!isAddOnEnabled('start')) {
306
+ if (options.typescript) {
307
+ await templateFile(templateDirRouter, './src/main.tsx.ejs', './src/main.tsx', {
308
+ routes,
309
+ integrations,
310
+ });
311
+ }
312
+ else {
313
+ await templateFile(templateDirRouter, './src/main.tsx.ejs', './src/main.jsx', {
314
+ routes,
315
+ integrations,
316
+ });
317
+ }
318
+ }
319
+ // Setup the app component. There are four variations, typescript/javascript and tailwind/non-tailwind.
320
+ if (options.mode === FILE_ROUTER) {
321
+ await templateFile(templateDirRouter, './src/routes/__root.tsx.ejs', './src/routes/__root.tsx', {
322
+ integrations,
323
+ });
324
+ await templateFile(templateDirBase, './src/App.tsx.ejs', './src/routes/index.tsx');
325
+ }
326
+ else {
327
+ await templateFile(templateDirBase, './src/App.tsx.ejs', options.typescript ? undefined : './src/App.jsx');
328
+ if (options.framework === 'react') {
329
+ await templateFile(templateDirBase, './src/App.test.tsx.ejs', options.typescript ? undefined : './src/App.test.jsx');
330
+ }
331
+ }
332
+ if (routes.length > 0) {
333
+ await templateFile(templateDirBase, './src/components/Header.tsx.ejs', './src/components/Header.tsx', {
334
+ integrations,
335
+ });
336
+ }
337
+ const warnings = [];
338
+ for (const addOn of options.chosenAddOns) {
339
+ if (addOn.warning) {
340
+ warnings.push(addOn.warning);
341
+ }
342
+ }
343
+ // Add .gitignore
344
+ await copyFile(resolve(templateDirBase, '_dot_gitignore'), resolve(targetDir, '.gitignore'));
345
+ // Create the README.md
346
+ await templateFile(templateDirBase, 'README.md.ejs');
347
+ // Install dependencies
348
+ s?.start(`Installing dependencies via ${options.packageManager}...`);
349
+ await execa(options.packageManager, ['install'], { cwd: targetDir });
350
+ s?.stop(`Installed dependencies`);
351
+ if (warnings.length > 0) {
352
+ if (!silent) {
353
+ log.warn(chalk.red(warnings.join('\n')));
354
+ }
355
+ }
356
+ if (options.git) {
357
+ s?.start(`Initializing git repository...`);
358
+ await execa('git', ['init'], { cwd: targetDir });
359
+ s?.stop(`Initialized git repository`);
360
+ }
361
+ if (!silent) {
362
+ outro(`Created your new TanStack app in '${basename(targetDir)}'.
363
+
364
+ Use the following commands to start your app:
365
+ % cd ${options.projectName}
366
+ % ${options.packageManager === 'deno' ? 'deno start' : options.packageManager} ${isAddOnEnabled('start') ? 'dev' : 'start'}
367
+
368
+ Please read README.md for more information on testing, styling, adding routes, react-query, etc.
369
+ `);
370
+ }
371
+ }