create-start-app 0.6.1 → 0.6.3

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.
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import { SUPPORTED_TOOLCHAINS } from './toolchain.js';
7
7
  import runServer from './mcp.js';
8
8
  import { listAddOns } from './add-ons.js';
9
9
  import { DEFAULT_FRAMEWORK, SUPPORTED_FRAMEWORKS } from './constants.js';
10
+ import { createDefaultEnvironment } from './environment.js';
10
11
  export function cli() {
11
12
  const program = new Command();
12
13
  program
@@ -72,7 +73,9 @@ export function cli() {
72
73
  intro("Let's configure your TanStack application");
73
74
  finalOptions = await promptForOptions(cliOptions);
74
75
  }
75
- await createApp(finalOptions);
76
+ await createApp(finalOptions, {
77
+ environment: createDefaultEnvironment(),
78
+ });
76
79
  }
77
80
  catch (error) {
78
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,7 +13,7 @@ function sortObject(obj) {
17
13
  return acc;
18
14
  }, {});
19
15
  }
20
- function createCopyFiles(targetDir) {
16
+ function createCopyFiles(environment, targetDir) {
21
17
  return async function copyFiles(templateDir, files,
22
18
  // optionally copy files from a folder to the root
23
19
  toRoot) {
@@ -27,7 +23,7 @@ function createCopyFiles(targetDir) {
27
23
  const fileNoPath = targetFileName.split('/').pop();
28
24
  targetFileName = fileNoPath ? `./${fileNoPath}` : targetFileName;
29
25
  }
30
- await copyFile(resolve(templateDir, file), resolve(targetDir, targetFileName));
26
+ await environment.copyFile(resolve(templateDir, file), resolve(targetDir, targetFileName));
31
27
  }
32
28
  };
33
29
  }
@@ -37,7 +33,7 @@ function jsSafeName(name) {
37
33
  .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
38
34
  .join('');
39
35
  }
40
- function createTemplateFile(projectName, options, targetDir) {
36
+ function createTemplateFile(environment, projectName, options, targetDir) {
41
37
  return async function templateFile(templateDir, file, targetFileName, extraTemplateValues) {
42
38
  const templateValues = {
43
39
  packageManager: options.packageManager,
@@ -56,7 +52,7 @@ function createTemplateFile(projectName, options, targetDir) {
56
52
  addOns: options.chosenAddOns,
57
53
  ...extraTemplateValues,
58
54
  };
59
- const template = await readFile(resolve(templateDir, file), 'utf-8');
55
+ const template = await environment.readFile(resolve(templateDir, file), 'utf-8');
60
56
  let content = '';
61
57
  try {
62
58
  content = render(template, templateValues);
@@ -75,17 +71,14 @@ function createTemplateFile(projectName, options, targetDir) {
75
71
  parser: 'typescript',
76
72
  });
77
73
  }
78
- await mkdir(dirname(resolve(targetDir, target)), {
79
- recursive: true,
80
- });
81
- await writeFile(resolve(targetDir, target), content);
74
+ await environment.writeFile(resolve(targetDir, target), content);
82
75
  };
83
76
  }
84
- async function createPackageJSON(projectName, options, templateDir, routerDir, targetDir, addOns) {
85
- 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'));
86
79
  packageJSON.name = projectName;
87
80
  if (options.typescript) {
88
- 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'));
89
82
  packageJSON = {
90
83
  ...packageJSON,
91
84
  devDependencies: {
@@ -95,7 +88,7 @@ async function createPackageJSON(projectName, options, templateDir, routerDir, t
95
88
  };
96
89
  }
97
90
  if (options.tailwind) {
98
- 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'));
99
92
  packageJSON = {
100
93
  ...packageJSON,
101
94
  dependencies: {
@@ -105,7 +98,7 @@ async function createPackageJSON(projectName, options, templateDir, routerDir, t
105
98
  };
106
99
  }
107
100
  if (options.toolchain === 'biome') {
108
- const biomePackageJSON = JSON.parse(await readFile(resolve(templateDir, 'package.biome.json'), 'utf8'));
101
+ const biomePackageJSON = JSON.parse(await environment.readFile(resolve(templateDir, 'package.biome.json'), 'utf8'));
109
102
  packageJSON = {
110
103
  ...packageJSON,
111
104
  scripts: {
@@ -119,7 +112,7 @@ async function createPackageJSON(projectName, options, templateDir, routerDir, t
119
112
  };
120
113
  }
121
114
  if (options.mode === FILE_ROUTER) {
122
- 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'));
123
116
  packageJSON = {
124
117
  ...packageJSON,
125
118
  dependencies: {
@@ -147,16 +140,15 @@ async function createPackageJSON(projectName, options, templateDir, routerDir, t
147
140
  }
148
141
  packageJSON.dependencies = sortObject(packageJSON.dependencies);
149
142
  packageJSON.devDependencies = sortObject(packageJSON.devDependencies);
150
- await writeFile(resolve(targetDir, 'package.json'), JSON.stringify(packageJSON, null, 2));
143
+ await environment.writeFile(resolve(targetDir, 'package.json'), JSON.stringify(packageJSON, null, 2));
151
144
  }
152
- async function copyFilesRecursively(source, target, copyFile, templateFile) {
153
- const sourceStat = statSync(source);
154
- if (sourceStat.isDirectory()) {
155
- const files = readdirSync(source);
145
+ async function copyFilesRecursively(environment, source, target, templateFile) {
146
+ if (environment.isDirectory(source)) {
147
+ const files = environment.readdir(source);
156
148
  for (const file of files) {
157
149
  const sourceChild = resolve(source, file);
158
150
  const targetChild = resolve(target, file);
159
- await copyFilesRecursively(sourceChild, targetChild, copyFile, templateFile);
151
+ await copyFilesRecursively(environment, sourceChild, targetChild, templateFile);
160
152
  }
161
153
  }
162
154
  else {
@@ -172,49 +164,43 @@ async function copyFilesRecursively(source, target, copyFile, templateFile) {
172
164
  isAppend = true;
173
165
  }
174
166
  const targetPath = resolve(dirname(target), targetFile);
175
- await mkdir(dirname(targetPath), {
176
- recursive: true,
177
- });
178
167
  if (isTemplate) {
179
168
  await templateFile(source, targetPath);
180
169
  }
181
170
  else {
182
171
  if (isAppend) {
183
- await appendFile(targetPath, (await readFile(source)).toString());
172
+ await environment.appendFile(targetPath, (await environment.readFile(source)).toString());
184
173
  }
185
174
  else {
186
- await copyFile(source, targetPath);
175
+ await environment.copyFile(source, targetPath);
187
176
  }
188
177
  }
189
178
  }
190
179
  }
191
- export async function createApp(options, { silent = false, } = {}) {
180
+ export async function createApp(options, { silent = false, environment, }) {
181
+ environment.startRun();
192
182
  const templateDirBase = fileURLToPath(new URL(`../templates/${options.framework}/base`, import.meta.url));
193
183
  const templateDirRouter = fileURLToPath(new URL(`../templates/${options.framework}/${options.mode}`, import.meta.url));
194
184
  const targetDir = resolve(process.cwd(), options.projectName);
195
- if (existsSync(targetDir)) {
185
+ if (environment.exists(targetDir)) {
196
186
  if (!silent) {
197
187
  log.error(`Directory "${options.projectName}" already exists`);
198
188
  }
199
189
  return;
200
190
  }
201
- const copyFiles = createCopyFiles(targetDir);
202
- const templateFile = createTemplateFile(options.projectName, options, targetDir);
191
+ const copyFiles = createCopyFiles(environment, targetDir);
192
+ const templateFile = createTemplateFile(environment, options.projectName, options, targetDir);
203
193
  const isAddOnEnabled = (id) => options.chosenAddOns.find((a) => a.id === id);
204
- // Make the root directory
205
- await mkdir(targetDir, { recursive: true });
206
194
  // Setup the .vscode directory
207
- await mkdir(resolve(targetDir, '.vscode'), { recursive: true });
208
195
  switch (options.toolchain) {
209
196
  case 'biome':
210
- await copyFile(resolve(templateDirBase, '_dot_vscode/settings.biome.json'), resolve(targetDir, '.vscode/settings.json'));
197
+ await environment.copyFile(resolve(templateDirBase, '_dot_vscode/settings.biome.json'), resolve(targetDir, '.vscode/settings.json'));
211
198
  break;
212
199
  case 'none':
213
200
  default:
214
- await copyFile(resolve(templateDirBase, '_dot_vscode/settings.json'), resolve(targetDir, '.vscode/settings.json'));
201
+ await environment.copyFile(resolve(templateDirBase, '_dot_vscode/settings.json'), resolve(targetDir, '.vscode/settings.json'));
215
202
  }
216
203
  // Fill the public directory
217
- await mkdir(resolve(targetDir, 'public'), { recursive: true });
218
204
  copyFiles(templateDirBase, [
219
205
  './public/robots.txt',
220
206
  './public/favicon.ico',
@@ -222,15 +208,9 @@ export async function createApp(options, { silent = false, } = {}) {
222
208
  './public/logo192.png',
223
209
  './public/logo512.png',
224
210
  ]);
225
- // Make the src directory
226
- await mkdir(resolve(targetDir, 'src'), { recursive: true });
227
- if (options.mode === FILE_ROUTER) {
228
- await mkdir(resolve(targetDir, 'src/routes'), { recursive: true });
229
- await mkdir(resolve(targetDir, 'src/components'), { recursive: true });
230
- }
231
211
  // Check for a .cursorrules file
232
- if (existsSync(resolve(templateDirBase, '.cursorrules'))) {
233
- await copyFile(resolve(templateDirBase, '.cursorrules'), resolve(targetDir, '.cursorrules'));
212
+ if (environment.exists(resolve(templateDirBase, '.cursorrules'))) {
213
+ await environment.copyFile(resolve(templateDirBase, '.cursorrules'), resolve(targetDir, '.cursorrules'));
234
214
  }
235
215
  // Copy in Vite and Tailwind config and CSS
236
216
  if (!options.tailwind) {
@@ -259,20 +239,18 @@ export async function createApp(options, { silent = false, } = {}) {
259
239
  await templateFile(templateDirBase, './tsconfig.json.ejs', './tsconfig.json');
260
240
  }
261
241
  // Setup the package.json file, optionally with typescript, tailwind and biome
262
- await createPackageJSON(options.projectName, options, templateDirBase, templateDirRouter, targetDir, options.chosenAddOns.map((addOn) => addOn.packageAdditions));
242
+ await createPackageJSON(environment, options.projectName, options, templateDirBase, templateDirRouter, targetDir, options.chosenAddOns.map((addOn) => addOn.packageAdditions));
263
243
  // Copy all the asset files from the addons
264
244
  const s = silent ? null : spinner();
265
245
  for (const phase of ['setup', 'add-on', 'example']) {
266
246
  for (const addOn of options.chosenAddOns.filter((addOn) => addOn.phase === phase)) {
267
247
  s?.start(`Setting up ${addOn.name}...`);
268
248
  const addOnDir = resolve(addOn.directory, 'assets');
269
- if (existsSync(addOnDir)) {
270
- await copyFilesRecursively(addOnDir, targetDir, copyFile, async (file, targetFileName) => templateFile(addOnDir, file, targetFileName));
249
+ if (environment.exists(addOnDir)) {
250
+ await copyFilesRecursively(environment, addOnDir, targetDir, async (file, targetFileName) => templateFile(addOnDir, file, targetFileName));
271
251
  }
272
252
  if (addOn.command) {
273
- await execa(addOn.command.command, addOn.command.args || [], {
274
- cwd: targetDir,
275
- });
253
+ await environment.execute(addOn.command.command, addOn.command.args || [], targetDir);
276
254
  }
277
255
  s?.stop(`${addOn.name} setup complete`);
278
256
  }
@@ -288,31 +266,29 @@ export async function createApp(options, { silent = false, } = {}) {
288
266
  }
289
267
  if (shadcnComponents.size > 0) {
290
268
  s?.start(`Installing shadcn components (${Array.from(shadcnComponents).join(', ')})...`);
291
- await execa('npx', ['shadcn@canary', 'add', ...shadcnComponents], {
292
- cwd: targetDir,
293
- });
269
+ await environment.execute('npx', ['shadcn@canary', 'add', ...shadcnComponents], targetDir);
294
270
  s?.stop(`Installed shadcn components`);
295
271
  }
296
272
  }
297
273
  const integrations = [];
298
- if (existsSync(resolve(targetDir, 'src/integrations'))) {
299
- for (const integration of readdirSync(resolve(targetDir, 'src/integrations'))) {
274
+ if (environment.exists(resolve(targetDir, 'src/integrations'))) {
275
+ for (const integration of environment.readdir(resolve(targetDir, 'src/integrations'))) {
300
276
  const integrationName = jsSafeName(integration);
301
- if (existsSync(resolve(targetDir, 'src/integrations', integration, 'layout.tsx'))) {
277
+ if (environment.exists(resolve(targetDir, 'src/integrations', integration, 'layout.tsx'))) {
302
278
  integrations.push({
303
279
  type: 'layout',
304
280
  name: `${integrationName}Layout`,
305
281
  path: `integrations/${integration}/layout`,
306
282
  });
307
283
  }
308
- if (existsSync(resolve(targetDir, 'src/integrations', integration, 'provider.tsx'))) {
284
+ if (environment.exists(resolve(targetDir, 'src/integrations', integration, 'provider.tsx'))) {
309
285
  integrations.push({
310
286
  type: 'provider',
311
287
  name: `${integrationName}Provider`,
312
288
  path: `integrations/${integration}/provider`,
313
289
  });
314
290
  }
315
- if (existsSync(resolve(targetDir, 'src/integrations', integration, 'header-user.tsx'))) {
291
+ if (environment.exists(resolve(targetDir, 'src/integrations', integration, 'header-user.tsx'))) {
316
292
  integrations.push({
317
293
  type: 'header-user',
318
294
  name: `${integrationName}Header`,
@@ -322,8 +298,8 @@ export async function createApp(options, { silent = false, } = {}) {
322
298
  }
323
299
  }
324
300
  const routes = [];
325
- if (existsSync(resolve(targetDir, 'src/routes'))) {
326
- for (const file of readdirSync(resolve(targetDir, 'src/routes'))) {
301
+ if (environment.exists(resolve(targetDir, 'src/routes'))) {
302
+ for (const file of environment.readdir(resolve(targetDir, 'src/routes'))) {
327
303
  const name = file.replace(/\.tsx?|\.jsx?/, '');
328
304
  const safeRouteName = jsSafeName(name);
329
305
  routes.push({
@@ -374,12 +350,12 @@ export async function createApp(options, { silent = false, } = {}) {
374
350
  }
375
351
  }
376
352
  // Add .gitignore
377
- await copyFile(resolve(templateDirBase, '_dot_gitignore'), resolve(targetDir, '.gitignore'));
353
+ await environment.copyFile(resolve(templateDirBase, '_dot_gitignore'), resolve(targetDir, '.gitignore'));
378
354
  // Create the README.md
379
355
  await templateFile(templateDirBase, 'README.md.ejs');
380
356
  // Install dependencies
381
357
  s?.start(`Installing dependencies via ${options.packageManager}...`);
382
- await execa(options.packageManager, ['install'], { cwd: targetDir });
358
+ await environment.execute(options.packageManager, ['install'], targetDir);
383
359
  s?.stop(`Installed dependencies`);
384
360
  if (warnings.length > 0) {
385
361
  if (!silent) {
@@ -391,23 +367,28 @@ export async function createApp(options, { silent = false, } = {}) {
391
367
  switch (options.packageManager) {
392
368
  case 'pnpm':
393
369
  // pnpm automatically forwards extra arguments
394
- await execa(options.packageManager, ['run', 'check', '--fix'], {
395
- cwd: targetDir,
396
- });
370
+ await environment.execute(options.packageManager, ['run', 'check', '--fix'], targetDir);
397
371
  break;
398
372
  default:
399
- await execa(options.packageManager, ['run', 'check', '--', '--fix'], {
400
- cwd: targetDir,
401
- });
373
+ await environment.execute(options.packageManager, ['run', 'check', '--', '--fix'], targetDir);
402
374
  break;
403
375
  }
404
376
  s?.stop(`Applied toolchain ${options.toolchain}...`);
405
377
  }
406
378
  if (options.git) {
407
379
  s?.start(`Initializing git repository...`);
408
- await execa('git', ['init'], { cwd: targetDir });
380
+ await environment.execute('git', ['init'], targetDir);
409
381
  s?.stop(`Initialized git repository`);
410
382
  }
383
+ environment.finishRun();
384
+ let errorStatement = '';
385
+ if (environment.getErrors().length) {
386
+ errorStatement = `
387
+
388
+ ${chalk.red('There were errors encountered during this process:')}
389
+
390
+ ${environment.getErrors().join('\n')}`;
391
+ }
411
392
  if (!silent) {
412
393
  outro(`Created your new TanStack app in '${basename(targetDir)}'.
413
394
 
@@ -415,7 +396,6 @@ Use the following commands to start your app:
415
396
  % cd ${options.projectName}
416
397
  % ${options.packageManager === 'deno' ? 'deno start' : options.packageManager} ${isAddOnEnabled('start') ? 'dev' : 'start'}
417
398
 
418
- Please read README.md for more information on testing, styling, adding routes, react-query, etc.
419
- `);
399
+ Please read README.md for more information on testing, styling, adding routes, react-query, etc.${errorStatement}`);
420
400
  }
421
401
  }
@@ -0,0 +1,43 @@
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
+ let errors = [];
7
+ return {
8
+ startRun: () => {
9
+ errors = [];
10
+ },
11
+ finishRun: () => { },
12
+ getErrors: () => errors,
13
+ appendFile: async (path, contents) => {
14
+ await mkdir(dirname(path), { recursive: true });
15
+ return appendFile(path, contents);
16
+ },
17
+ copyFile: async (from, to) => {
18
+ await mkdir(dirname(to), { recursive: true });
19
+ return copyFile(from, to);
20
+ },
21
+ writeFile: async (path, contents) => {
22
+ await mkdir(dirname(path), { recursive: true });
23
+ return writeFile(path, contents);
24
+ },
25
+ execute: async (command, args, cwd) => {
26
+ try {
27
+ await execa(command, args, {
28
+ cwd,
29
+ });
30
+ }
31
+ catch {
32
+ errors.push(`Command "${command} ${args.join(' ')}" did not run successfully. Please run this manually in your project.`);
33
+ }
34
+ },
35
+ readFile: (path, encoding) => readFile(path, { encoding: encoding || 'utf8' }),
36
+ exists: (path) => existsSync(path),
37
+ readdir: (path) => readdirSync(path),
38
+ isDirectory: (path) => {
39
+ const stat = statSync(path);
40
+ return stat.isDirectory();
41
+ },
42
+ };
43
+ }
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',
@@ -93,6 +94,7 @@ server.tool('createTanStackReactApplication', {
93
94
  variableValues: {},
94
95
  }, {
95
96
  silent: true,
97
+ environment: createDefaultEnvironment(),
96
98
  });
97
99
  return {
98
100
  content: [{ type: 'text', text: 'Application created successfully' }],
@@ -170,6 +172,7 @@ server.tool('createTanStackSolidApplication', {
170
172
  variableValues: {},
171
173
  }, {
172
174
  silent: true,
175
+ environment: createDefaultEnvironment(),
173
176
  });
174
177
  return {
175
178
  content: [{ type: 'text', text: 'Application created successfully' }],
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "create-start-app",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
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
@@ -10,6 +10,8 @@ import runServer from './mcp.js'
10
10
  import { listAddOns } from './add-ons.js'
11
11
  import { DEFAULT_FRAMEWORK, SUPPORTED_FRAMEWORKS } from './constants.js'
12
12
 
13
+ import { createDefaultEnvironment } from './environment.js'
14
+
13
15
  import type { PackageManager } from './package-manager.js'
14
16
  import type { ToolChain } from './toolchain.js'
15
17
  import type { CliOptions, Framework } from './types.js'
@@ -115,7 +117,9 @@ export function cli() {
115
117
  intro("Let's configure your TanStack application")
116
118
  finalOptions = await promptForOptions(cliOptions)
117
119
  }
118
- await createApp(finalOptions)
120
+ await createApp(finalOptions, {
121
+ environment: createDefaultEnvironment(),
122
+ })
119
123
  } catch (error) {
120
124
  log.error(
121
125
  error instanceof Error