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
@@ -0,0 +1,53 @@
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
+ function isDirectory(path) {
6
+ return statSync(path).isDirectory();
7
+ }
8
+ export async function getAllAddOns() {
9
+ const addOns = [];
10
+ for (const type of ['add-on', 'example']) {
11
+ const addOnsBase = fileURLToPath(new URL(`../templates/${type}`, import.meta.url));
12
+ for (const dir of await readdirSync(addOnsBase).filter((file) => isDirectory(resolve(addOnsBase, file)))) {
13
+ const filePath = resolve(addOnsBase, dir, 'info.json');
14
+ const fileContent = await readFile(filePath, 'utf-8');
15
+ let packageAdditions = {};
16
+ if (existsSync(resolve(addOnsBase, dir, 'package.json'))) {
17
+ packageAdditions = JSON.parse(await readFile(resolve(addOnsBase, dir, 'package.json'), 'utf-8'));
18
+ }
19
+ let readme;
20
+ if (existsSync(resolve(addOnsBase, dir, 'README.md'))) {
21
+ readme = await readFile(resolve(addOnsBase, dir, 'README.md'), 'utf-8');
22
+ }
23
+ addOns.push({
24
+ id: dir,
25
+ type,
26
+ ...JSON.parse(fileContent),
27
+ directory: resolve(addOnsBase, dir),
28
+ packageAdditions,
29
+ readme,
30
+ });
31
+ }
32
+ }
33
+ return addOns;
34
+ }
35
+ // Turn the list of chosen add-on IDs into a final list of add-ons by resolving dependencies
36
+ export async function finalizeAddOns(chosenAddOnIDs) {
37
+ const finalAddOnIDs = new Set(chosenAddOnIDs);
38
+ const addOns = await getAllAddOns();
39
+ for (const addOnID of finalAddOnIDs) {
40
+ const addOn = addOns.find((a) => a.id === addOnID);
41
+ if (!addOn) {
42
+ throw new Error(`Add-on ${addOnID} not found`);
43
+ }
44
+ for (const dependsOn of addOn.dependsOn || []) {
45
+ const dep = addOns.find((a) => a.id === dependsOn);
46
+ if (!dep) {
47
+ throw new Error(`Dependency ${dependsOn} not found`);
48
+ }
49
+ finalAddOnIDs.add(dep.id);
50
+ }
51
+ }
52
+ return [...finalAddOnIDs].map((id) => addOns.find((a) => a.id === id));
53
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,51 @@
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
+ export function cli() {
7
+ const program = new Command();
8
+ program
9
+ .name('create-tsrouter-app')
10
+ .description('CLI to create a new TanStack application')
11
+ .argument('[project-name]', 'name of the project')
12
+ .option('--no-git', 'do not create a git repository')
13
+ .option('--template <type>', 'project template (typescript, javascript, file-router)', (value) => {
14
+ if (value !== 'typescript' &&
15
+ value !== 'javascript' &&
16
+ value !== 'file-router') {
17
+ throw new InvalidArgumentError(`Invalid template: ${value}. Only the following are allowed: typescript, javascript, file-router`);
18
+ }
19
+ return value;
20
+ })
21
+ .option(`--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`, `Explicitly tell the CLI to use this package manager`, (value) => {
22
+ if (!SUPPORTED_PACKAGE_MANAGERS.includes(value)) {
23
+ throw new InvalidArgumentError(`Invalid package manager: ${value}. The following are allowed: ${SUPPORTED_PACKAGE_MANAGERS.join(', ')}`);
24
+ }
25
+ return value;
26
+ })
27
+ .option('--tailwind', 'add Tailwind CSS', false)
28
+ .option('--add-ons', 'pick from a list of available add-ons', false)
29
+ .action(async (projectName, options) => {
30
+ try {
31
+ const cliOptions = {
32
+ projectName,
33
+ ...options,
34
+ };
35
+ let finalOptions = normalizeOptions(cliOptions);
36
+ if (finalOptions) {
37
+ intro(`Creating a new TanStack app in ${projectName}...`);
38
+ }
39
+ else {
40
+ intro("Let's configure your TanStack application");
41
+ finalOptions = await promptForOptions(cliOptions);
42
+ }
43
+ await createApp(finalOptions);
44
+ }
45
+ catch (error) {
46
+ log.error(error instanceof Error ? error.message : 'An unknown error occurred');
47
+ process.exit(1);
48
+ }
49
+ });
50
+ program.parse();
51
+ }
@@ -0,0 +1,2 @@
1
+ export const CODE_ROUTER = 'code-router';
2
+ export const FILE_ROUTER = 'file-router';
@@ -0,0 +1,285 @@
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 createTemplateFile(projectName, options, targetDir) {
29
+ return async function templateFile(templateDir, file, targetFileName) {
30
+ const templateValues = {
31
+ packageManager: options.packageManager,
32
+ projectName: projectName,
33
+ typescript: options.typescript,
34
+ tailwind: options.tailwind,
35
+ js: options.typescript ? 'ts' : 'js',
36
+ jsx: options.typescript ? 'tsx' : 'jsx',
37
+ fileRouter: options.mode === FILE_ROUTER,
38
+ codeRouter: options.mode === CODE_ROUTER,
39
+ addOnEnabled: options.chosenAddOns.reduce((acc, addOn) => {
40
+ acc[addOn.id] = true;
41
+ return acc;
42
+ }, {}),
43
+ addOns: options.chosenAddOns,
44
+ };
45
+ const template = await readFile(resolve(templateDir, file), 'utf-8');
46
+ let content = render(template, templateValues);
47
+ const target = targetFileName ?? file.replace('.ejs', '');
48
+ if (target.endsWith('.ts') || target.endsWith('.tsx')) {
49
+ content = await format(content, {
50
+ semi: false,
51
+ singleQuote: true,
52
+ trailingComma: 'all',
53
+ parser: 'typescript',
54
+ });
55
+ }
56
+ await mkdir(dirname(resolve(targetDir, target)), {
57
+ recursive: true,
58
+ });
59
+ await writeFile(resolve(targetDir, target), content);
60
+ };
61
+ }
62
+ async function createPackageJSON(projectName, options, templateDir, routerDir, targetDir, addOns) {
63
+ let packageJSON = JSON.parse(await readFile(resolve(templateDir, 'package.json'), 'utf8'));
64
+ packageJSON.name = projectName;
65
+ if (options.typescript) {
66
+ const tsPackageJSON = JSON.parse(await readFile(resolve(templateDir, 'package.ts.json'), 'utf8'));
67
+ packageJSON = {
68
+ ...packageJSON,
69
+ devDependencies: {
70
+ ...packageJSON.devDependencies,
71
+ ...tsPackageJSON.devDependencies,
72
+ },
73
+ };
74
+ }
75
+ if (options.tailwind) {
76
+ const twPackageJSON = JSON.parse(await readFile(resolve(templateDir, 'package.tw.json'), 'utf8'));
77
+ packageJSON = {
78
+ ...packageJSON,
79
+ dependencies: {
80
+ ...packageJSON.dependencies,
81
+ ...twPackageJSON.dependencies,
82
+ },
83
+ };
84
+ }
85
+ if (options.mode === FILE_ROUTER) {
86
+ const frPackageJSON = JSON.parse(await readFile(resolve(routerDir, 'package.fr.json'), 'utf8'));
87
+ packageJSON = {
88
+ ...packageJSON,
89
+ dependencies: {
90
+ ...packageJSON.dependencies,
91
+ ...frPackageJSON.dependencies,
92
+ },
93
+ };
94
+ }
95
+ for (const addOn of addOns) {
96
+ packageJSON = {
97
+ ...packageJSON,
98
+ dependencies: {
99
+ ...packageJSON.dependencies,
100
+ ...addOn.dependencies,
101
+ },
102
+ devDependencies: {
103
+ ...packageJSON.devDependencies,
104
+ ...addOn.devDependencies,
105
+ },
106
+ scripts: {
107
+ ...packageJSON.scripts,
108
+ ...addOn.scripts,
109
+ },
110
+ };
111
+ }
112
+ packageJSON.dependencies = sortObject(packageJSON.dependencies);
113
+ packageJSON.devDependencies = sortObject(packageJSON.devDependencies);
114
+ await writeFile(resolve(targetDir, 'package.json'), JSON.stringify(packageJSON, null, 2));
115
+ }
116
+ async function copyFilesRecursively(source, target, copyFile, templateFile) {
117
+ const sourceStat = statSync(source);
118
+ if (sourceStat.isDirectory()) {
119
+ const files = readdirSync(source);
120
+ for (const file of files) {
121
+ const sourceChild = resolve(source, file);
122
+ const targetChild = resolve(target, file);
123
+ await copyFilesRecursively(sourceChild, targetChild, copyFile, templateFile);
124
+ }
125
+ }
126
+ else {
127
+ if (source.endsWith('.ejs')) {
128
+ const targetPath = target.replace('.ejs', '');
129
+ await mkdir(dirname(targetPath), {
130
+ recursive: true,
131
+ });
132
+ await templateFile(source, targetPath);
133
+ }
134
+ else {
135
+ await mkdir(dirname(target), {
136
+ recursive: true,
137
+ });
138
+ if (source.endsWith('.append')) {
139
+ await appendFile(target.replace('.append', ''), (await readFile(source)).toString());
140
+ }
141
+ else {
142
+ await copyFile(source, target);
143
+ }
144
+ }
145
+ }
146
+ }
147
+ export async function createApp(options) {
148
+ const templateDirBase = fileURLToPath(new URL('../templates/base', import.meta.url));
149
+ const templateDirRouter = fileURLToPath(new URL(`../templates/${options.mode}`, import.meta.url));
150
+ const targetDir = resolve(process.cwd(), options.projectName);
151
+ if (existsSync(targetDir)) {
152
+ log.error(`Directory "${options.projectName}" already exists`);
153
+ return;
154
+ }
155
+ const copyFiles = createCopyFiles(targetDir);
156
+ const templateFile = createTemplateFile(options.projectName, options, targetDir);
157
+ const isAddOnEnabled = (id) => options.chosenAddOns.find((a) => a.id === id);
158
+ log.info(`Creating a new TanStack app in '${basename(targetDir)}'...`);
159
+ // Make the root directory
160
+ await mkdir(targetDir, { recursive: true });
161
+ // Setup the .vscode directory
162
+ await mkdir(resolve(targetDir, '.vscode'), { recursive: true });
163
+ await copyFile(resolve(templateDirBase, '.vscode/settings.json'), resolve(targetDir, '.vscode/settings.json'));
164
+ // Fill the public directory
165
+ await mkdir(resolve(targetDir, 'public'), { recursive: true });
166
+ copyFiles(templateDirBase, [
167
+ './public/robots.txt',
168
+ './public/favicon.ico',
169
+ './public/manifest.json',
170
+ './public/logo192.png',
171
+ './public/logo512.png',
172
+ ]);
173
+ // Make the src directory
174
+ await mkdir(resolve(targetDir, 'src'), { recursive: true });
175
+ if (options.mode === FILE_ROUTER) {
176
+ await mkdir(resolve(targetDir, 'src/routes'), { recursive: true });
177
+ await mkdir(resolve(targetDir, 'src/components'), { recursive: true });
178
+ }
179
+ // Copy in Vite and Tailwind config and CSS
180
+ if (!options.tailwind) {
181
+ await copyFiles(templateDirBase, ['./src/App.css']);
182
+ }
183
+ await templateFile(templateDirBase, './vite.config.js.ejs');
184
+ await templateFile(templateDirBase, './src/styles.css.ejs');
185
+ copyFiles(templateDirBase, ['./src/logo.svg']);
186
+ // Setup the app component. There are four variations, typescript/javascript and tailwind/non-tailwind.
187
+ if (options.mode === FILE_ROUTER) {
188
+ await templateFile(templateDirRouter, './src/components/Header.tsx.ejs', './src/components/Header.tsx');
189
+ await templateFile(templateDirRouter, './src/routes/__root.tsx.ejs', './src/routes/__root.tsx');
190
+ await templateFile(templateDirBase, './src/App.tsx.ejs', './src/routes/index.tsx');
191
+ }
192
+ else {
193
+ await templateFile(templateDirBase, './src/App.tsx.ejs', options.typescript ? undefined : './src/App.jsx');
194
+ await templateFile(templateDirBase, './src/App.test.tsx.ejs', options.typescript ? undefined : './src/App.test.jsx');
195
+ }
196
+ // Create the main entry point
197
+ if (!isAddOnEnabled('start')) {
198
+ if (options.typescript) {
199
+ await templateFile(templateDirRouter, './src/main.tsx.ejs');
200
+ }
201
+ else {
202
+ await templateFile(templateDirRouter, './src/main.tsx.ejs', './src/main.jsx');
203
+ }
204
+ }
205
+ // Setup the main, reportWebVitals and index.html files
206
+ if (!isAddOnEnabled('start')) {
207
+ if (options.typescript) {
208
+ await templateFile(templateDirBase, './src/reportWebVitals.ts.ejs');
209
+ }
210
+ else {
211
+ await templateFile(templateDirBase, './src/reportWebVitals.ts.ejs', './src/reportWebVitals.js');
212
+ }
213
+ await templateFile(templateDirBase, './index.html.ejs');
214
+ }
215
+ // Setup tsconfig
216
+ if (options.typescript) {
217
+ await templateFile(templateDirBase, './tsconfig.json.ejs', './tsconfig.json');
218
+ }
219
+ // Setup the package.json file, optionally with typescript and tailwind
220
+ await createPackageJSON(options.projectName, options, templateDirBase, templateDirRouter, targetDir, options.chosenAddOns.map((addOn) => addOn.packageAdditions));
221
+ // Copy all the asset files from the addons
222
+ const s = spinner();
223
+ for (const phase of ['setup', 'add-on', 'example']) {
224
+ for (const addOn of options.chosenAddOns.filter((addOn) => addOn.phase === phase)) {
225
+ s.start(`Setting up ${addOn.name}...`);
226
+ const addOnDir = resolve(addOn.directory, 'assets');
227
+ if (existsSync(addOnDir)) {
228
+ await copyFilesRecursively(addOnDir, targetDir, copyFile, async (file, targetFileName) => templateFile(addOnDir, file, targetFileName));
229
+ }
230
+ if (addOn.command) {
231
+ await execa(addOn.command.command, addOn.command.args || [], {
232
+ cwd: targetDir,
233
+ });
234
+ }
235
+ s.stop(`${addOn.name} setup complete`);
236
+ }
237
+ }
238
+ if (isAddOnEnabled('shadcn')) {
239
+ const shadcnComponents = new Set();
240
+ for (const addOn of options.chosenAddOns) {
241
+ if (addOn.shadcnComponents) {
242
+ for (const component of addOn.shadcnComponents) {
243
+ shadcnComponents.add(component);
244
+ }
245
+ }
246
+ }
247
+ if (shadcnComponents.size > 0) {
248
+ s.start(`Installing shadcn components (${Array.from(shadcnComponents).join(', ')})...`);
249
+ await execa('npx', ['shadcn@canary', 'add', ...shadcnComponents], {
250
+ cwd: targetDir,
251
+ });
252
+ s.stop(`Installed shadcn components`);
253
+ }
254
+ }
255
+ const warnings = [];
256
+ for (const addOn of options.chosenAddOns) {
257
+ if (addOn.warning) {
258
+ warnings.push(addOn.warning);
259
+ }
260
+ }
261
+ // Add .gitignore
262
+ await copyFile(resolve(templateDirBase, 'gitignore'), resolve(targetDir, '.gitignore'));
263
+ // Create the README.md
264
+ await templateFile(templateDirBase, 'README.md.ejs');
265
+ // Install dependencies
266
+ s.start(`Installing dependencies via ${options.packageManager}...`);
267
+ await execa(options.packageManager, ['install'], { cwd: targetDir });
268
+ s.stop(`Installed dependencies`);
269
+ if (warnings.length > 0) {
270
+ log.warn(chalk.red(warnings.join('\n')));
271
+ }
272
+ if (options.git) {
273
+ s.start(`Initializing git repository...`);
274
+ await execa('git', ['init'], { cwd: targetDir });
275
+ s.stop(`Initialized git repository`);
276
+ }
277
+ outro(`Created your new TanStack app in '${basename(targetDir)}'.
278
+
279
+ Use the following commands to start your app:
280
+ % cd ${options.projectName}
281
+ % ${options.packageManager} ${isAddOnEnabled('start') ? 'dev' : 'start'}
282
+
283
+ Please read README.md for more information on testing, styling, adding routes, react-query, etc.
284
+ `);
285
+ }