create-start-app 0.6.1 → 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.
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,42 @@ 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, }) {
192
181
  const templateDirBase = fileURLToPath(new URL(`../templates/${options.framework}/base`, import.meta.url));
193
182
  const templateDirRouter = fileURLToPath(new URL(`../templates/${options.framework}/${options.mode}`, import.meta.url));
194
183
  const targetDir = resolve(process.cwd(), options.projectName);
195
- if (existsSync(targetDir)) {
184
+ if (environment.exists(targetDir)) {
196
185
  if (!silent) {
197
186
  log.error(`Directory "${options.projectName}" already exists`);
198
187
  }
199
188
  return;
200
189
  }
201
- const copyFiles = createCopyFiles(targetDir);
202
- const templateFile = createTemplateFile(options.projectName, options, targetDir);
190
+ const copyFiles = createCopyFiles(environment, targetDir);
191
+ const templateFile = createTemplateFile(environment, options.projectName, options, targetDir);
203
192
  const isAddOnEnabled = (id) => options.chosenAddOns.find((a) => a.id === id);
204
- // Make the root directory
205
- await mkdir(targetDir, { recursive: true });
206
193
  // Setup the .vscode directory
207
- await mkdir(resolve(targetDir, '.vscode'), { recursive: true });
208
194
  switch (options.toolchain) {
209
195
  case 'biome':
210
- await copyFile(resolve(templateDirBase, '_dot_vscode/settings.biome.json'), resolve(targetDir, '.vscode/settings.json'));
196
+ await environment.copyFile(resolve(templateDirBase, '_dot_vscode/settings.biome.json'), resolve(targetDir, '.vscode/settings.json'));
211
197
  break;
212
198
  case 'none':
213
199
  default:
214
- await copyFile(resolve(templateDirBase, '_dot_vscode/settings.json'), resolve(targetDir, '.vscode/settings.json'));
200
+ await environment.copyFile(resolve(templateDirBase, '_dot_vscode/settings.json'), resolve(targetDir, '.vscode/settings.json'));
215
201
  }
216
202
  // Fill the public directory
217
- await mkdir(resolve(targetDir, 'public'), { recursive: true });
218
203
  copyFiles(templateDirBase, [
219
204
  './public/robots.txt',
220
205
  './public/favicon.ico',
@@ -222,15 +207,9 @@ export async function createApp(options, { silent = false, } = {}) {
222
207
  './public/logo192.png',
223
208
  './public/logo512.png',
224
209
  ]);
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
210
  // Check for a .cursorrules file
232
- if (existsSync(resolve(templateDirBase, '.cursorrules'))) {
233
- 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'));
234
213
  }
235
214
  // Copy in Vite and Tailwind config and CSS
236
215
  if (!options.tailwind) {
@@ -259,20 +238,18 @@ export async function createApp(options, { silent = false, } = {}) {
259
238
  await templateFile(templateDirBase, './tsconfig.json.ejs', './tsconfig.json');
260
239
  }
261
240
  // 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));
241
+ await createPackageJSON(environment, options.projectName, options, templateDirBase, templateDirRouter, targetDir, options.chosenAddOns.map((addOn) => addOn.packageAdditions));
263
242
  // Copy all the asset files from the addons
264
243
  const s = silent ? null : spinner();
265
244
  for (const phase of ['setup', 'add-on', 'example']) {
266
245
  for (const addOn of options.chosenAddOns.filter((addOn) => addOn.phase === phase)) {
267
246
  s?.start(`Setting up ${addOn.name}...`);
268
247
  const addOnDir = resolve(addOn.directory, 'assets');
269
- if (existsSync(addOnDir)) {
270
- 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));
271
250
  }
272
251
  if (addOn.command) {
273
- await execa(addOn.command.command, addOn.command.args || [], {
274
- cwd: targetDir,
275
- });
252
+ await environment.execute(addOn.command.command, addOn.command.args || [], targetDir);
276
253
  }
277
254
  s?.stop(`${addOn.name} setup complete`);
278
255
  }
@@ -288,31 +265,29 @@ export async function createApp(options, { silent = false, } = {}) {
288
265
  }
289
266
  if (shadcnComponents.size > 0) {
290
267
  s?.start(`Installing shadcn components (${Array.from(shadcnComponents).join(', ')})...`);
291
- await execa('npx', ['shadcn@canary', 'add', ...shadcnComponents], {
292
- cwd: targetDir,
293
- });
268
+ await environment.execute('npx', ['shadcn@canary', 'add', ...shadcnComponents], targetDir);
294
269
  s?.stop(`Installed shadcn components`);
295
270
  }
296
271
  }
297
272
  const integrations = [];
298
- if (existsSync(resolve(targetDir, 'src/integrations'))) {
299
- 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'))) {
300
275
  const integrationName = jsSafeName(integration);
301
- if (existsSync(resolve(targetDir, 'src/integrations', integration, 'layout.tsx'))) {
276
+ if (environment.exists(resolve(targetDir, 'src/integrations', integration, 'layout.tsx'))) {
302
277
  integrations.push({
303
278
  type: 'layout',
304
279
  name: `${integrationName}Layout`,
305
280
  path: `integrations/${integration}/layout`,
306
281
  });
307
282
  }
308
- if (existsSync(resolve(targetDir, 'src/integrations', integration, 'provider.tsx'))) {
283
+ if (environment.exists(resolve(targetDir, 'src/integrations', integration, 'provider.tsx'))) {
309
284
  integrations.push({
310
285
  type: 'provider',
311
286
  name: `${integrationName}Provider`,
312
287
  path: `integrations/${integration}/provider`,
313
288
  });
314
289
  }
315
- if (existsSync(resolve(targetDir, 'src/integrations', integration, 'header-user.tsx'))) {
290
+ if (environment.exists(resolve(targetDir, 'src/integrations', integration, 'header-user.tsx'))) {
316
291
  integrations.push({
317
292
  type: 'header-user',
318
293
  name: `${integrationName}Header`,
@@ -322,8 +297,8 @@ export async function createApp(options, { silent = false, } = {}) {
322
297
  }
323
298
  }
324
299
  const routes = [];
325
- if (existsSync(resolve(targetDir, 'src/routes'))) {
326
- 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'))) {
327
302
  const name = file.replace(/\.tsx?|\.jsx?/, '');
328
303
  const safeRouteName = jsSafeName(name);
329
304
  routes.push({
@@ -374,12 +349,12 @@ export async function createApp(options, { silent = false, } = {}) {
374
349
  }
375
350
  }
376
351
  // Add .gitignore
377
- await copyFile(resolve(templateDirBase, '_dot_gitignore'), resolve(targetDir, '.gitignore'));
352
+ await environment.copyFile(resolve(templateDirBase, '_dot_gitignore'), resolve(targetDir, '.gitignore'));
378
353
  // Create the README.md
379
354
  await templateFile(templateDirBase, 'README.md.ejs');
380
355
  // Install dependencies
381
356
  s?.start(`Installing dependencies via ${options.packageManager}...`);
382
- await execa(options.packageManager, ['install'], { cwd: targetDir });
357
+ await environment.execute(options.packageManager, ['install'], targetDir);
383
358
  s?.stop(`Installed dependencies`);
384
359
  if (warnings.length > 0) {
385
360
  if (!silent) {
@@ -391,21 +366,17 @@ export async function createApp(options, { silent = false, } = {}) {
391
366
  switch (options.packageManager) {
392
367
  case 'pnpm':
393
368
  // pnpm automatically forwards extra arguments
394
- await execa(options.packageManager, ['run', 'check', '--fix'], {
395
- cwd: targetDir,
396
- });
369
+ await environment.execute(options.packageManager, ['run', 'check', '--fix'], targetDir);
397
370
  break;
398
371
  default:
399
- await execa(options.packageManager, ['run', 'check', '--', '--fix'], {
400
- cwd: targetDir,
401
- });
372
+ await environment.execute(options.packageManager, ['run', 'check', '--', '--fix'], targetDir);
402
373
  break;
403
374
  }
404
375
  s?.stop(`Applied toolchain ${options.toolchain}...`);
405
376
  }
406
377
  if (options.git) {
407
378
  s?.start(`Initializing git repository...`);
408
- await execa('git', ['init'], { cwd: targetDir });
379
+ await environment.execute('git', ['init'], targetDir);
409
380
  s?.stop(`Initialized git repository`);
410
381
  }
411
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',
@@ -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.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
@@ -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