create-start-app 0.5.0 → 0.6.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 (44) hide show
  1. package/README.md +8 -0
  2. package/dist/cli.js +11 -1
  3. package/dist/create-app.js +86 -67
  4. package/dist/environment.js +32 -0
  5. package/dist/mcp.js +22 -1
  6. package/dist/options.js +22 -1
  7. package/dist/toolchain.js +2 -0
  8. package/package.json +4 -2
  9. package/src/cli.ts +21 -1
  10. package/src/create-app.ts +134 -77
  11. package/src/environment.ts +53 -0
  12. package/src/mcp.ts +24 -1
  13. package/src/options.ts +22 -1
  14. package/src/toolchain.ts +3 -0
  15. package/src/types.ts +3 -0
  16. package/templates/react/add-on/form/assets/src/components/demo.FormComponents.tsx +120 -0
  17. package/templates/react/add-on/form/assets/src/hooks/demo.form-context.ts +4 -0
  18. package/templates/react/add-on/form/assets/src/hooks/demo.form.ts +22 -0
  19. package/templates/react/add-on/form/assets/src/routes/demo.form.address.tsx.ejs +203 -0
  20. package/templates/react/add-on/form/assets/src/routes/demo.form.simple.tsx.ejs +79 -0
  21. package/templates/react/add-on/form/info.json +6 -2
  22. package/templates/react/add-on/form/package.json +2 -1
  23. package/templates/react/base/README.md.ejs +11 -1
  24. package/templates/react/base/_dot_vscode/settings.biome.json +38 -0
  25. package/templates/react/base/package.biome.json +10 -0
  26. package/templates/react/base/toolchain/biome.json +31 -0
  27. package/templates/react/code-router/src/main.tsx.ejs +2 -2
  28. package/templates/react/example/tanchat/info.json +1 -1
  29. package/templates/react/file-router/src/main.tsx.ejs +2 -2
  30. package/templates/solid/add-on/form/assets/src/routes/demo.form.tsx.ejs +310 -106
  31. package/templates/solid/add-on/form/package.json +1 -1
  32. package/templates/solid/base/_dot_vscode/settings.biome.json +38 -0
  33. package/templates/solid/base/package.biome.json +10 -0
  34. package/templates/solid/base/toolchain/biome.json +31 -0
  35. package/templates/solid/code-router/src/main.tsx.ejs +4 -2
  36. package/templates/solid/file-router/src/main.tsx.ejs +4 -2
  37. package/templates/solid/file-router/src/routes/__root.tsx.ejs +1 -1
  38. package/tests/cra.test.ts +112 -0
  39. package/tests/snapshots/cra/cr-js-npm.json +34 -0
  40. package/tests/snapshots/cra/cr-ts-npm.json +35 -0
  41. package/tests/snapshots/cra/fr-ts-npm.json +35 -0
  42. package/tests/snapshots/cra/fr-ts-tw-npm.json +34 -0
  43. package/tests/test-utilities.ts +69 -0
  44. package/templates/react/add-on/form/assets/src/routes/demo.form.tsx.ejs +0 -62
package/README.md CHANGED
@@ -30,6 +30,7 @@ This will start an interactive CLI that guides you through the setup process, al
30
30
  - TypeScript support
31
31
  - Tailwind CSS integration
32
32
  - Package manager
33
+ - Toolchain
33
34
  - Git initialization
34
35
 
35
36
  ## Command Line Options
@@ -45,6 +46,7 @@ Available options:
45
46
  - `--template <type>`: Choose between `file-router`, `typescript`, or `javascript`
46
47
  - `--tailwind`: Enable Tailwind CSS
47
48
  - `--package-manager`: Specify your preferred package manager (`npm`, `yarn`, `pnpm`, `bun`, or `deno`)
49
+ - `--toolchain`: Specify your toolchain solution for formatting/linting (`biome`)
48
50
  - `--no-git`: Do not initialize a git repository
49
51
  - `--add-ons`: Enable add-on selection or specify add-ons to install
50
52
 
@@ -94,6 +96,12 @@ Choose your preferred package manager (`npm`, `bun`, `yarn`, `pnpm`, or `deno`)
94
96
 
95
97
  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.
96
98
 
99
+ ### Toolchain
100
+
101
+ Choose your preferred solution for formatting and linting either through the interactive CLI or using the `--toolchain` flag.
102
+
103
+ Setting this flag to `biome` will configure it as your toolchain of choice, adding a `biome.json` to the root of the project. Consult the [biome documentation](https://biomejs.dev/guides/getting-started/) for further customization.
104
+
97
105
  ## Add-ons (experimental)
98
106
 
99
107
  You can enable add-on selection:
package/dist/cli.js CHANGED
@@ -3,9 +3,11 @@ import { intro, log } from '@clack/prompts';
3
3
  import { createApp } from './create-app.js';
4
4
  import { normalizeOptions, promptForOptions } from './options.js';
5
5
  import { SUPPORTED_PACKAGE_MANAGERS } from './package-manager.js';
6
+ import { SUPPORTED_TOOLCHAINS } from './toolchain.js';
6
7
  import runServer from './mcp.js';
7
8
  import { listAddOns } from './add-ons.js';
8
9
  import { DEFAULT_FRAMEWORK, SUPPORTED_FRAMEWORKS } from './constants.js';
10
+ import { createDefaultEnvironment } from './environment.js';
9
11
  export function cli() {
10
12
  const program = new Command();
11
13
  program
@@ -32,6 +34,12 @@ export function cli() {
32
34
  throw new InvalidArgumentError(`Invalid package manager: ${value}. The following are allowed: ${SUPPORTED_PACKAGE_MANAGERS.join(', ')}`);
33
35
  }
34
36
  return value;
37
+ })
38
+ .option(`--toolchain <${SUPPORTED_TOOLCHAINS.join('|')}>`, `Explicitly tell the CLI to use this toolchain`, (value) => {
39
+ if (!SUPPORTED_TOOLCHAINS.includes(value)) {
40
+ throw new InvalidArgumentError(`Invalid toolchain: ${value}. The following are allowed: ${SUPPORTED_TOOLCHAINS.join(', ')}`);
41
+ }
42
+ return value;
35
43
  })
36
44
  .option('--tailwind', 'add Tailwind CSS', false)
37
45
  .option('--add-ons [...add-ons]', 'pick from a list of available add-ons (comma separated list)', (value) => {
@@ -65,7 +73,9 @@ export function cli() {
65
73
  intro("Let's configure your TanStack application");
66
74
  finalOptions = await promptForOptions(cliOptions);
67
75
  }
68
- await createApp(finalOptions);
76
+ await createApp(finalOptions, {
77
+ environment: createDefaultEnvironment(),
78
+ });
69
79
  }
70
80
  catch (error) {
71
81
  log.error(error instanceof Error
@@ -1,10 +1,6 @@
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
1
  import { basename, dirname, resolve } from 'node:path';
5
2
  import { fileURLToPath } from 'node:url';
6
3
  import { log, outro, spinner } from '@clack/prompts';
7
- import { execa } from 'execa';
8
4
  import { render } from 'ejs';
9
5
  import { format } from 'prettier';
10
6
  import chalk from 'chalk';
@@ -17,11 +13,17 @@ function sortObject(obj) {
17
13
  return acc;
18
14
  }, {});
19
15
  }
20
- function createCopyFiles(targetDir) {
21
- return async function copyFiles(templateDir, files) {
16
+ function createCopyFiles(environment, targetDir) {
17
+ return async function copyFiles(templateDir, files,
18
+ // optionally copy files from a folder to the root
19
+ toRoot) {
22
20
  for (const file of files) {
23
- const targetFileName = file.replace('.tw', '');
24
- await copyFile(resolve(templateDir, file), resolve(targetDir, targetFileName));
21
+ let targetFileName = file.replace('.tw', '');
22
+ if (toRoot) {
23
+ const fileNoPath = targetFileName.split('/').pop();
24
+ targetFileName = fileNoPath ? `./${fileNoPath}` : targetFileName;
25
+ }
26
+ await environment.copyFile(resolve(templateDir, file), resolve(targetDir, targetFileName));
25
27
  }
26
28
  };
27
29
  }
@@ -31,13 +33,14 @@ function jsSafeName(name) {
31
33
  .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
32
34
  .join('');
33
35
  }
34
- function createTemplateFile(projectName, options, targetDir) {
36
+ function createTemplateFile(environment, projectName, options, targetDir) {
35
37
  return async function templateFile(templateDir, file, targetFileName, extraTemplateValues) {
36
38
  const templateValues = {
37
39
  packageManager: options.packageManager,
38
40
  projectName: projectName,
39
41
  typescript: options.typescript,
40
42
  tailwind: options.tailwind,
43
+ toolchain: options.toolchain,
41
44
  js: options.typescript ? 'ts' : 'js',
42
45
  jsx: options.typescript ? 'tsx' : 'jsx',
43
46
  fileRouter: options.mode === FILE_ROUTER,
@@ -49,7 +52,7 @@ function createTemplateFile(projectName, options, targetDir) {
49
52
  addOns: options.chosenAddOns,
50
53
  ...extraTemplateValues,
51
54
  };
52
- const template = await readFile(resolve(templateDir, file), 'utf-8');
55
+ const template = await environment.readFile(resolve(templateDir, file), 'utf-8');
53
56
  let content = '';
54
57
  try {
55
58
  content = render(template, templateValues);
@@ -68,17 +71,14 @@ function createTemplateFile(projectName, options, targetDir) {
68
71
  parser: 'typescript',
69
72
  });
70
73
  }
71
- await mkdir(dirname(resolve(targetDir, target)), {
72
- recursive: true,
73
- });
74
- await writeFile(resolve(targetDir, target), content);
74
+ await environment.writeFile(resolve(targetDir, target), content);
75
75
  };
76
76
  }
77
- async function createPackageJSON(projectName, options, templateDir, routerDir, targetDir, addOns) {
78
- let packageJSON = JSON.parse(await readFile(resolve(templateDir, 'package.json'), 'utf8'));
77
+ async function createPackageJSON(environment, projectName, options, templateDir, routerDir, targetDir, addOns) {
78
+ let packageJSON = JSON.parse(await environment.readFile(resolve(templateDir, 'package.json'), 'utf8'));
79
79
  packageJSON.name = projectName;
80
80
  if (options.typescript) {
81
- const tsPackageJSON = JSON.parse(await readFile(resolve(templateDir, 'package.ts.json'), 'utf8'));
81
+ const tsPackageJSON = JSON.parse(await environment.readFile(resolve(templateDir, 'package.ts.json'), 'utf8'));
82
82
  packageJSON = {
83
83
  ...packageJSON,
84
84
  devDependencies: {
@@ -88,7 +88,7 @@ async function createPackageJSON(projectName, options, templateDir, routerDir, t
88
88
  };
89
89
  }
90
90
  if (options.tailwind) {
91
- const twPackageJSON = JSON.parse(await readFile(resolve(templateDir, 'package.tw.json'), 'utf8'));
91
+ const twPackageJSON = JSON.parse(await environment.readFile(resolve(templateDir, 'package.tw.json'), 'utf8'));
92
92
  packageJSON = {
93
93
  ...packageJSON,
94
94
  dependencies: {
@@ -97,8 +97,22 @@ async function createPackageJSON(projectName, options, templateDir, routerDir, t
97
97
  },
98
98
  };
99
99
  }
100
+ if (options.toolchain === 'biome') {
101
+ const biomePackageJSON = JSON.parse(await environment.readFile(resolve(templateDir, 'package.biome.json'), 'utf8'));
102
+ packageJSON = {
103
+ ...packageJSON,
104
+ scripts: {
105
+ ...packageJSON.scripts,
106
+ ...biomePackageJSON.scripts,
107
+ },
108
+ devDependencies: {
109
+ ...packageJSON.devDependencies,
110
+ ...biomePackageJSON.devDependencies,
111
+ },
112
+ };
113
+ }
100
114
  if (options.mode === FILE_ROUTER) {
101
- const frPackageJSON = JSON.parse(await readFile(resolve(routerDir, 'package.fr.json'), 'utf8'));
115
+ const frPackageJSON = JSON.parse(await environment.readFile(resolve(routerDir, 'package.fr.json'), 'utf8'));
102
116
  packageJSON = {
103
117
  ...packageJSON,
104
118
  dependencies: {
@@ -126,16 +140,15 @@ async function createPackageJSON(projectName, options, templateDir, routerDir, t
126
140
  }
127
141
  packageJSON.dependencies = sortObject(packageJSON.dependencies);
128
142
  packageJSON.devDependencies = sortObject(packageJSON.devDependencies);
129
- await writeFile(resolve(targetDir, 'package.json'), JSON.stringify(packageJSON, null, 2));
143
+ await environment.writeFile(resolve(targetDir, 'package.json'), JSON.stringify(packageJSON, null, 2));
130
144
  }
131
- async function copyFilesRecursively(source, target, copyFile, templateFile) {
132
- const sourceStat = statSync(source);
133
- if (sourceStat.isDirectory()) {
134
- const files = readdirSync(source);
145
+ async function copyFilesRecursively(environment, source, target, templateFile) {
146
+ if (environment.isDirectory(source)) {
147
+ const files = environment.readdir(source);
135
148
  for (const file of files) {
136
149
  const sourceChild = resolve(source, file);
137
150
  const targetChild = resolve(target, file);
138
- await copyFilesRecursively(sourceChild, targetChild, copyFile, templateFile);
151
+ await copyFilesRecursively(environment, sourceChild, targetChild, templateFile);
139
152
  }
140
153
  }
141
154
  else {
@@ -151,42 +164,42 @@ async function copyFilesRecursively(source, target, copyFile, templateFile) {
151
164
  isAppend = true;
152
165
  }
153
166
  const targetPath = resolve(dirname(target), targetFile);
154
- await mkdir(dirname(targetPath), {
155
- recursive: true,
156
- });
157
167
  if (isTemplate) {
158
168
  await templateFile(source, targetPath);
159
169
  }
160
170
  else {
161
171
  if (isAppend) {
162
- await appendFile(targetPath, (await readFile(source)).toString());
172
+ await environment.appendFile(targetPath, (await environment.readFile(source)).toString());
163
173
  }
164
174
  else {
165
- await copyFile(source, targetPath);
175
+ await environment.copyFile(source, targetPath);
166
176
  }
167
177
  }
168
178
  }
169
179
  }
170
- export async function createApp(options, { silent = false, } = {}) {
180
+ export async function createApp(options, { silent = false, environment, }) {
171
181
  const templateDirBase = fileURLToPath(new URL(`../templates/${options.framework}/base`, import.meta.url));
172
182
  const templateDirRouter = fileURLToPath(new URL(`../templates/${options.framework}/${options.mode}`, import.meta.url));
173
183
  const targetDir = resolve(process.cwd(), options.projectName);
174
- if (existsSync(targetDir)) {
184
+ if (environment.exists(targetDir)) {
175
185
  if (!silent) {
176
186
  log.error(`Directory "${options.projectName}" already exists`);
177
187
  }
178
188
  return;
179
189
  }
180
- const copyFiles = createCopyFiles(targetDir);
181
- const templateFile = createTemplateFile(options.projectName, options, targetDir);
190
+ const copyFiles = createCopyFiles(environment, targetDir);
191
+ const templateFile = createTemplateFile(environment, options.projectName, options, targetDir);
182
192
  const isAddOnEnabled = (id) => options.chosenAddOns.find((a) => a.id === id);
183
- // Make the root directory
184
- await mkdir(targetDir, { recursive: true });
185
193
  // 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'));
194
+ switch (options.toolchain) {
195
+ case 'biome':
196
+ await environment.copyFile(resolve(templateDirBase, '_dot_vscode/settings.biome.json'), resolve(targetDir, '.vscode/settings.json'));
197
+ break;
198
+ case 'none':
199
+ default:
200
+ await environment.copyFile(resolve(templateDirBase, '_dot_vscode/settings.json'), resolve(targetDir, '.vscode/settings.json'));
201
+ }
188
202
  // Fill the public directory
189
- await mkdir(resolve(targetDir, 'public'), { recursive: true });
190
203
  copyFiles(templateDirBase, [
191
204
  './public/robots.txt',
192
205
  './public/favicon.ico',
@@ -194,15 +207,9 @@ export async function createApp(options, { silent = false, } = {}) {
194
207
  './public/logo192.png',
195
208
  './public/logo512.png',
196
209
  ]);
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
210
  // Check for a .cursorrules file
204
- if (existsSync(resolve(templateDirBase, '.cursorrules'))) {
205
- await copyFile(resolve(templateDirBase, '.cursorrules'), resolve(targetDir, '.cursorrules'));
211
+ if (environment.exists(resolve(templateDirBase, '.cursorrules'))) {
212
+ await environment.copyFile(resolve(templateDirBase, '.cursorrules'), resolve(targetDir, '.cursorrules'));
206
213
  }
207
214
  // Copy in Vite and Tailwind config and CSS
208
215
  if (!options.tailwind) {
@@ -211,6 +218,9 @@ export async function createApp(options, { silent = false, } = {}) {
211
218
  await templateFile(templateDirBase, './vite.config.js.ejs');
212
219
  await templateFile(templateDirBase, './src/styles.css.ejs');
213
220
  copyFiles(templateDirBase, ['./src/logo.svg']);
221
+ if (options.toolchain === 'biome') {
222
+ copyFiles(templateDirBase, ['./toolchain/biome.json'], true);
223
+ }
214
224
  // Setup the main, reportWebVitals and index.html files
215
225
  if (!isAddOnEnabled('start') && options.framework === 'react') {
216
226
  if (options.typescript) {
@@ -227,21 +237,19 @@ export async function createApp(options, { silent = false, } = {}) {
227
237
  if (options.typescript) {
228
238
  await templateFile(templateDirBase, './tsconfig.json.ejs', './tsconfig.json');
229
239
  }
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));
240
+ // Setup the package.json file, optionally with typescript, tailwind and biome
241
+ await createPackageJSON(environment, options.projectName, options, templateDirBase, templateDirRouter, targetDir, options.chosenAddOns.map((addOn) => addOn.packageAdditions));
232
242
  // Copy all the asset files from the addons
233
243
  const s = silent ? null : spinner();
234
244
  for (const phase of ['setup', 'add-on', 'example']) {
235
245
  for (const addOn of options.chosenAddOns.filter((addOn) => addOn.phase === phase)) {
236
246
  s?.start(`Setting up ${addOn.name}...`);
237
247
  const addOnDir = resolve(addOn.directory, 'assets');
238
- if (existsSync(addOnDir)) {
239
- await copyFilesRecursively(addOnDir, targetDir, copyFile, async (file, targetFileName) => templateFile(addOnDir, file, targetFileName));
248
+ if (environment.exists(addOnDir)) {
249
+ await copyFilesRecursively(environment, addOnDir, targetDir, async (file, targetFileName) => templateFile(addOnDir, file, targetFileName));
240
250
  }
241
251
  if (addOn.command) {
242
- await execa(addOn.command.command, addOn.command.args || [], {
243
- cwd: targetDir,
244
- });
252
+ await environment.execute(addOn.command.command, addOn.command.args || [], targetDir);
245
253
  }
246
254
  s?.stop(`${addOn.name} setup complete`);
247
255
  }
@@ -257,31 +265,29 @@ export async function createApp(options, { silent = false, } = {}) {
257
265
  }
258
266
  if (shadcnComponents.size > 0) {
259
267
  s?.start(`Installing shadcn components (${Array.from(shadcnComponents).join(', ')})...`);
260
- await execa('npx', ['shadcn@canary', 'add', ...shadcnComponents], {
261
- cwd: targetDir,
262
- });
268
+ await environment.execute('npx', ['shadcn@canary', 'add', ...shadcnComponents], targetDir);
263
269
  s?.stop(`Installed shadcn components`);
264
270
  }
265
271
  }
266
272
  const integrations = [];
267
- if (existsSync(resolve(targetDir, 'src/integrations'))) {
268
- for (const integration of readdirSync(resolve(targetDir, 'src/integrations'))) {
273
+ if (environment.exists(resolve(targetDir, 'src/integrations'))) {
274
+ for (const integration of environment.readdir(resolve(targetDir, 'src/integrations'))) {
269
275
  const integrationName = jsSafeName(integration);
270
- if (existsSync(resolve(targetDir, 'src/integrations', integration, 'layout.tsx'))) {
276
+ if (environment.exists(resolve(targetDir, 'src/integrations', integration, 'layout.tsx'))) {
271
277
  integrations.push({
272
278
  type: 'layout',
273
279
  name: `${integrationName}Layout`,
274
280
  path: `integrations/${integration}/layout`,
275
281
  });
276
282
  }
277
- if (existsSync(resolve(targetDir, 'src/integrations', integration, 'provider.tsx'))) {
283
+ if (environment.exists(resolve(targetDir, 'src/integrations', integration, 'provider.tsx'))) {
278
284
  integrations.push({
279
285
  type: 'provider',
280
286
  name: `${integrationName}Provider`,
281
287
  path: `integrations/${integration}/provider`,
282
288
  });
283
289
  }
284
- if (existsSync(resolve(targetDir, 'src/integrations', integration, 'header-user.tsx'))) {
290
+ if (environment.exists(resolve(targetDir, 'src/integrations', integration, 'header-user.tsx'))) {
285
291
  integrations.push({
286
292
  type: 'header-user',
287
293
  name: `${integrationName}Header`,
@@ -291,8 +297,8 @@ export async function createApp(options, { silent = false, } = {}) {
291
297
  }
292
298
  }
293
299
  const routes = [];
294
- if (existsSync(resolve(targetDir, 'src/routes'))) {
295
- for (const file of readdirSync(resolve(targetDir, 'src/routes'))) {
300
+ if (environment.exists(resolve(targetDir, 'src/routes'))) {
301
+ for (const file of environment.readdir(resolve(targetDir, 'src/routes'))) {
296
302
  const name = file.replace(/\.tsx?|\.jsx?/, '');
297
303
  const safeRouteName = jsSafeName(name);
298
304
  routes.push({
@@ -343,21 +349,34 @@ export async function createApp(options, { silent = false, } = {}) {
343
349
  }
344
350
  }
345
351
  // Add .gitignore
346
- await copyFile(resolve(templateDirBase, '_dot_gitignore'), resolve(targetDir, '.gitignore'));
352
+ await environment.copyFile(resolve(templateDirBase, '_dot_gitignore'), resolve(targetDir, '.gitignore'));
347
353
  // Create the README.md
348
354
  await templateFile(templateDirBase, 'README.md.ejs');
349
355
  // Install dependencies
350
356
  s?.start(`Installing dependencies via ${options.packageManager}...`);
351
- await execa(options.packageManager, ['install'], { cwd: targetDir });
357
+ await environment.execute(options.packageManager, ['install'], targetDir);
352
358
  s?.stop(`Installed dependencies`);
353
359
  if (warnings.length > 0) {
354
360
  if (!silent) {
355
361
  log.warn(chalk.red(warnings.join('\n')));
356
362
  }
357
363
  }
364
+ if (options.toolchain === 'biome') {
365
+ s?.start(`Applying toolchain ${options.toolchain}...`);
366
+ switch (options.packageManager) {
367
+ case 'pnpm':
368
+ // pnpm automatically forwards extra arguments
369
+ await environment.execute(options.packageManager, ['run', 'check', '--fix'], targetDir);
370
+ break;
371
+ default:
372
+ await environment.execute(options.packageManager, ['run', 'check', '--', '--fix'], targetDir);
373
+ break;
374
+ }
375
+ s?.stop(`Applied toolchain ${options.toolchain}...`);
376
+ }
358
377
  if (options.git) {
359
378
  s?.start(`Initializing git repository...`);
360
- await execa('git', ['init'], { cwd: targetDir });
379
+ await environment.execute('git', ['init'], targetDir);
361
380
  s?.stop(`Initialized git repository`);
362
381
  }
363
382
  if (!silent) {
@@ -0,0 +1,32 @@
1
+ import { appendFile, copyFile, mkdir, readFile, writeFile, } from 'node:fs/promises';
2
+ import { existsSync, readdirSync, statSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { execa } from 'execa';
5
+ export function createDefaultEnvironment() {
6
+ return {
7
+ appendFile: async (path, contents) => {
8
+ await mkdir(dirname(path), { recursive: true });
9
+ return appendFile(path, contents);
10
+ },
11
+ copyFile: async (from, to) => {
12
+ await mkdir(dirname(to), { recursive: true });
13
+ return copyFile(from, to);
14
+ },
15
+ writeFile: async (path, contents) => {
16
+ await mkdir(dirname(path), { recursive: true });
17
+ return writeFile(path, contents);
18
+ },
19
+ execute: async (command, args, cwd) => {
20
+ await execa(command, args, {
21
+ cwd,
22
+ });
23
+ },
24
+ readFile: (path, encoding) => readFile(path, { encoding: encoding || 'utf8' }),
25
+ exists: (path) => existsSync(path),
26
+ readdir: (path) => readdirSync(path),
27
+ isDirectory: (path) => {
28
+ const stat = statSync(path);
29
+ return stat.isDirectory();
30
+ },
31
+ };
32
+ }
package/dist/mcp.js CHANGED
@@ -5,6 +5,7 @@ import express from 'express';
5
5
  import { z } from 'zod';
6
6
  import { createApp } from './create-app.js';
7
7
  import { finalizeAddOns } from './add-ons.js';
8
+ import { createDefaultEnvironment } from './environment.js';
8
9
  const server = new McpServer({
9
10
  name: 'Demo',
10
11
  version: '1.0.0',
@@ -46,6 +47,10 @@ const tanStackReactAddOns = [
46
47
  id: 'store',
47
48
  description: 'Enable the TanStack Store state management library',
48
49
  },
50
+ {
51
+ id: 'tanchat',
52
+ description: 'Add an AI chatbot example to the application',
53
+ },
49
54
  ];
50
55
  server.tool('listTanStackReactAddOns', {}, () => {
51
56
  return {
@@ -68,6 +73,7 @@ server.tool('createTanStackReactApplication', {
68
73
  'start',
69
74
  'store',
70
75
  'tanstack-query',
76
+ 'tanchat',
71
77
  ]))
72
78
  .describe('The IDs of the add-ons to install'),
73
79
  }, async ({ projectName, addOns, cwd }) => {
@@ -80,6 +86,7 @@ server.tool('createTanStackReactApplication', {
80
86
  typescript: true,
81
87
  tailwind: true,
82
88
  packageManager: 'pnpm',
89
+ toolchain: 'none',
83
90
  mode: 'file-router',
84
91
  addOns: true,
85
92
  chosenAddOns,
@@ -87,6 +94,7 @@ server.tool('createTanStackReactApplication', {
87
94
  variableValues: {},
88
95
  }, {
89
96
  silent: true,
97
+ environment: createDefaultEnvironment(),
90
98
  });
91
99
  return {
92
100
  content: [{ type: 'text', text: 'Application created successfully' }],
@@ -121,6 +129,10 @@ const tanStackSolidAddOns = [
121
129
  id: 'tanstack-query',
122
130
  description: 'Enable TanStack Query for data fetching',
123
131
  },
132
+ {
133
+ id: 'tanchat',
134
+ description: 'Add an AI chatbot example to the application',
135
+ },
124
136
  ];
125
137
  server.tool('listTanStackSolidAddOns', {}, () => {
126
138
  return {
@@ -133,7 +145,14 @@ server.tool('createTanStackSolidApplication', {
133
145
  .describe('The package.json module name of the application (will also be the directory name)'),
134
146
  cwd: z.string().describe('The directory to create the application in'),
135
147
  addOns: z
136
- .array(z.enum(['solid-ui', 'form', 'sentry', 'store', 'tanstack-query']))
148
+ .array(z.enum([
149
+ 'solid-ui',
150
+ 'form',
151
+ 'sentry',
152
+ 'store',
153
+ 'tanstack-query',
154
+ 'tanchat',
155
+ ]))
137
156
  .describe('The IDs of the add-ons to install'),
138
157
  }, async ({ projectName, addOns, cwd }) => {
139
158
  try {
@@ -145,6 +164,7 @@ server.tool('createTanStackSolidApplication', {
145
164
  typescript: true,
146
165
  tailwind: true,
147
166
  packageManager: 'pnpm',
167
+ toolchain: 'none',
148
168
  mode: 'file-router',
149
169
  addOns: true,
150
170
  chosenAddOns,
@@ -152,6 +172,7 @@ server.tool('createTanStackSolidApplication', {
152
172
  variableValues: {},
153
173
  }, {
154
174
  silent: true,
175
+ environment: createDefaultEnvironment(),
155
176
  });
156
177
  return {
157
178
  content: [{ type: 'text', text: 'Application created successfully' }],
package/dist/options.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { cancel, confirm, isCancel, multiselect, select, text, } from '@clack/prompts';
2
2
  import { DEFAULT_PACKAGE_MANAGER, SUPPORTED_PACKAGE_MANAGERS, getPackageManager, } from './package-manager.js';
3
+ import { DEFAULT_TOOLCHAIN, SUPPORTED_TOOLCHAINS } from './toolchain.js';
3
4
  import { CODE_ROUTER, DEFAULT_FRAMEWORK, FILE_ROUTER } from './constants.js';
4
5
  import { finalizeAddOns, getAllAddOns } from './add-ons.js';
5
6
  // If all CLI options are provided, use them directly
@@ -26,6 +27,7 @@ export async function normalizeOptions(cliOptions) {
26
27
  typescript,
27
28
  tailwind,
28
29
  packageManager: cliOptions.packageManager || DEFAULT_PACKAGE_MANAGER,
30
+ toolchain: cliOptions.toolchain || DEFAULT_TOOLCHAIN,
29
31
  mode: cliOptions.template === 'file-router' ? FILE_ROUTER : CODE_ROUTER,
30
32
  git: !!cliOptions.git,
31
33
  addOns,
@@ -145,7 +147,7 @@ export async function promptForOptions(cliOptions) {
145
147
  }
146
148
  }
147
149
  // Tailwind selection
148
- if (cliOptions.tailwind === undefined && options.framework === 'react') {
150
+ if (!cliOptions.tailwind && options.framework === 'react') {
149
151
  const tailwind = await confirm({
150
152
  message: 'Would you like to use Tailwind CSS?',
151
153
  initialValue: true,
@@ -184,6 +186,25 @@ export async function promptForOptions(cliOptions) {
184
186
  else {
185
187
  options.packageManager = cliOptions.packageManager;
186
188
  }
189
+ // Toolchain selection
190
+ if (cliOptions.toolchain === undefined) {
191
+ const tc = await select({
192
+ message: 'Select toolchain',
193
+ options: SUPPORTED_TOOLCHAINS.map((tc) => ({
194
+ value: tc,
195
+ label: tc,
196
+ })),
197
+ initialValue: DEFAULT_TOOLCHAIN,
198
+ });
199
+ if (isCancel(tc)) {
200
+ cancel('Operation cancelled.');
201
+ process.exit(0);
202
+ }
203
+ options.toolchain = tc;
204
+ }
205
+ else {
206
+ options.toolchain = cliOptions.toolchain;
207
+ }
187
208
  options.chosenAddOns = [];
188
209
  if (Array.isArray(cliOptions.addOns)) {
189
210
  options.chosenAddOns = await finalizeAddOns(options.framework, options.mode, cliOptions.addOns);
@@ -0,0 +1,2 @@
1
+ export const SUPPORTED_TOOLCHAINS = ['none', 'biome'];
2
+ export const DEFAULT_TOOLCHAIN = 'none';
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "create-start-app",
3
- "version": "0.5.0",
3
+ "version": "0.6.2",
4
4
  "description": "Tanstack Application Builder",
5
5
  "bin": "./dist/index.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "build": "tsc",
9
9
  "start": "tsc && node dist/index.js",
10
- "test": "npm run test:lint",
10
+ "test": "npm run test:lint && vitest --run",
11
+ "test:watch": "vitest",
11
12
  "cipublish": "node scripts/publish.js",
12
13
  "test:lint": "eslint ./src",
13
14
  "mcp": "tsc && npx @modelcontextprotocol/inspector dist/index.js --mcp"
@@ -39,6 +40,7 @@
39
40
  "execa": "^9.5.2",
40
41
  "express": "^4.21.2",
41
42
  "prettier": "^3.5.0",
43
+ "vitest": "^3.0.8",
42
44
  "zod": "^3.24.2"
43
45
  },
44
46
  "devDependencies": {
package/src/cli.ts CHANGED
@@ -4,12 +4,16 @@ import { intro, log } from '@clack/prompts'
4
4
  import { createApp } from './create-app.js'
5
5
  import { normalizeOptions, promptForOptions } from './options.js'
6
6
  import { SUPPORTED_PACKAGE_MANAGERS } from './package-manager.js'
7
+ import { SUPPORTED_TOOLCHAINS } from './toolchain.js'
7
8
 
8
9
  import runServer from './mcp.js'
9
10
  import { listAddOns } from './add-ons.js'
10
11
  import { DEFAULT_FRAMEWORK, SUPPORTED_FRAMEWORKS } from './constants.js'
11
12
 
13
+ import { createDefaultEnvironment } from './environment.js'
14
+
12
15
  import type { PackageManager } from './package-manager.js'
16
+ import type { ToolChain } from './toolchain.js'
13
17
  import type { CliOptions, Framework } from './types.js'
14
18
 
15
19
  export function cli() {
@@ -65,6 +69,20 @@ export function cli() {
65
69
  return value as PackageManager
66
70
  },
67
71
  )
72
+ .option<ToolChain>(
73
+ `--toolchain <${SUPPORTED_TOOLCHAINS.join('|')}>`,
74
+ `Explicitly tell the CLI to use this toolchain`,
75
+ (value) => {
76
+ if (!SUPPORTED_TOOLCHAINS.includes(value as ToolChain)) {
77
+ throw new InvalidArgumentError(
78
+ `Invalid toolchain: ${value}. The following are allowed: ${SUPPORTED_TOOLCHAINS.join(
79
+ ', ',
80
+ )}`,
81
+ )
82
+ }
83
+ return value as ToolChain
84
+ },
85
+ )
68
86
  .option('--tailwind', 'add Tailwind CSS', false)
69
87
  .option<Array<string> | boolean>(
70
88
  '--add-ons [...add-ons]',
@@ -99,7 +117,9 @@ export function cli() {
99
117
  intro("Let's configure your TanStack application")
100
118
  finalOptions = await promptForOptions(cliOptions)
101
119
  }
102
- await createApp(finalOptions)
120
+ await createApp(finalOptions, {
121
+ environment: createDefaultEnvironment(),
122
+ })
103
123
  } catch (error) {
104
124
  log.error(
105
125
  error instanceof Error