create-tsrouter-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/src/create-app.ts CHANGED
@@ -1,23 +1,13 @@
1
- #!/usr/bin/env node
2
-
3
- import {
4
- appendFile,
5
- copyFile,
6
- mkdir,
7
- readFile,
8
- writeFile,
9
- } from 'node:fs/promises'
10
- import { existsSync, readdirSync, statSync } from 'node:fs'
11
1
  import { basename, dirname, resolve } from 'node:path'
12
2
  import { fileURLToPath } from 'node:url'
13
3
  import { log, outro, spinner } from '@clack/prompts'
14
- import { execa } from 'execa'
15
4
  import { render } from 'ejs'
16
5
  import { format } from 'prettier'
17
6
  import chalk from 'chalk'
18
7
 
19
8
  import { CODE_ROUTER, FILE_ROUTER } from './constants.js'
20
9
 
10
+ import type { Environment } from './environment.js'
21
11
  import type { Options } from './types.js'
22
12
 
23
13
  function sortObject(obj: Record<string, string>): Record<string, string> {
@@ -29,7 +19,7 @@ function sortObject(obj: Record<string, string>): Record<string, string> {
29
19
  }, {})
30
20
  }
31
21
 
32
- function createCopyFiles(targetDir: string) {
22
+ function createCopyFiles(environment: Environment, targetDir: string) {
33
23
  return async function copyFiles(
34
24
  templateDir: string,
35
25
  files: Array<string>,
@@ -42,7 +32,7 @@ function createCopyFiles(targetDir: string) {
42
32
  const fileNoPath = targetFileName.split('/').pop()
43
33
  targetFileName = fileNoPath ? `./${fileNoPath}` : targetFileName
44
34
  }
45
- await copyFile(
35
+ await environment.copyFile(
46
36
  resolve(templateDir, file),
47
37
  resolve(targetDir, targetFileName),
48
38
  )
@@ -58,6 +48,7 @@ function jsSafeName(name: string) {
58
48
  }
59
49
 
60
50
  function createTemplateFile(
51
+ environment: Environment,
61
52
  projectName: string,
62
53
  options: Required<Options>,
63
54
  targetDir: string,
@@ -89,7 +80,10 @@ function createTemplateFile(
89
80
  ...extraTemplateValues,
90
81
  }
91
82
 
92
- const template = await readFile(resolve(templateDir, file), 'utf-8')
83
+ const template = await environment.readFile(
84
+ resolve(templateDir, file),
85
+ 'utf-8',
86
+ )
93
87
  let content = ''
94
88
  try {
95
89
  content = render(template, templateValues)
@@ -109,15 +103,12 @@ function createTemplateFile(
109
103
  })
110
104
  }
111
105
 
112
- await mkdir(dirname(resolve(targetDir, target)), {
113
- recursive: true,
114
- })
115
-
116
- await writeFile(resolve(targetDir, target), content)
106
+ await environment.writeFile(resolve(targetDir, target), content)
117
107
  }
118
108
  }
119
109
 
120
110
  async function createPackageJSON(
111
+ environment: Environment,
121
112
  projectName: string,
122
113
  options: Required<Options>,
123
114
  templateDir: string,
@@ -130,12 +121,15 @@ async function createPackageJSON(
130
121
  }>,
131
122
  ) {
132
123
  let packageJSON = JSON.parse(
133
- await readFile(resolve(templateDir, 'package.json'), 'utf8'),
124
+ await environment.readFile(resolve(templateDir, 'package.json'), 'utf8'),
134
125
  )
135
126
  packageJSON.name = projectName
136
127
  if (options.typescript) {
137
128
  const tsPackageJSON = JSON.parse(
138
- await readFile(resolve(templateDir, 'package.ts.json'), 'utf8'),
129
+ await environment.readFile(
130
+ resolve(templateDir, 'package.ts.json'),
131
+ 'utf8',
132
+ ),
139
133
  )
140
134
  packageJSON = {
141
135
  ...packageJSON,
@@ -147,7 +141,10 @@ async function createPackageJSON(
147
141
  }
148
142
  if (options.tailwind) {
149
143
  const twPackageJSON = JSON.parse(
150
- await readFile(resolve(templateDir, 'package.tw.json'), 'utf8'),
144
+ await environment.readFile(
145
+ resolve(templateDir, 'package.tw.json'),
146
+ 'utf8',
147
+ ),
151
148
  )
152
149
  packageJSON = {
153
150
  ...packageJSON,
@@ -159,7 +156,10 @@ async function createPackageJSON(
159
156
  }
160
157
  if (options.toolchain === 'biome') {
161
158
  const biomePackageJSON = JSON.parse(
162
- await readFile(resolve(templateDir, 'package.biome.json'), 'utf8'),
159
+ await environment.readFile(
160
+ resolve(templateDir, 'package.biome.json'),
161
+ 'utf8',
162
+ ),
163
163
  )
164
164
  packageJSON = {
165
165
  ...packageJSON,
@@ -175,7 +175,7 @@ async function createPackageJSON(
175
175
  }
176
176
  if (options.mode === FILE_ROUTER) {
177
177
  const frPackageJSON = JSON.parse(
178
- await readFile(resolve(routerDir, 'package.fr.json'), 'utf8'),
178
+ await environment.readFile(resolve(routerDir, 'package.fr.json'), 'utf8'),
179
179
  )
180
180
  packageJSON = {
181
181
  ...packageJSON,
@@ -211,28 +211,27 @@ async function createPackageJSON(
211
211
  packageJSON.devDependencies as Record<string, string>,
212
212
  )
213
213
 
214
- await writeFile(
214
+ await environment.writeFile(
215
215
  resolve(targetDir, 'package.json'),
216
216
  JSON.stringify(packageJSON, null, 2),
217
217
  )
218
218
  }
219
219
 
220
220
  async function copyFilesRecursively(
221
+ environment: Environment,
221
222
  source: string,
222
223
  target: string,
223
- copyFile: (source: string, target: string) => Promise<void>,
224
224
  templateFile: (file: string, targetFileName?: string) => Promise<void>,
225
225
  ) {
226
- const sourceStat = statSync(source)
227
- if (sourceStat.isDirectory()) {
228
- const files = readdirSync(source)
226
+ if (environment.isDirectory(source)) {
227
+ const files = environment.readdir(source)
229
228
  for (const file of files) {
230
229
  const sourceChild = resolve(source, file)
231
230
  const targetChild = resolve(target, file)
232
231
  await copyFilesRecursively(
232
+ environment,
233
233
  sourceChild,
234
234
  targetChild,
235
- copyFile,
236
235
  templateFile,
237
236
  )
238
237
  }
@@ -251,17 +250,16 @@ async function copyFilesRecursively(
251
250
 
252
251
  const targetPath = resolve(dirname(target), targetFile)
253
252
 
254
- await mkdir(dirname(targetPath), {
255
- recursive: true,
256
- })
257
-
258
253
  if (isTemplate) {
259
254
  await templateFile(source, targetPath)
260
255
  } else {
261
256
  if (isAppend) {
262
- await appendFile(targetPath, (await readFile(source)).toString())
257
+ await environment.appendFile(
258
+ targetPath,
259
+ (await environment.readFile(source)).toString(),
260
+ )
263
261
  } else {
264
- await copyFile(source, targetPath)
262
+ await environment.copyFile(source, targetPath)
265
263
  }
266
264
  }
267
265
  }
@@ -271,9 +269,11 @@ export async function createApp(
271
269
  options: Required<Options>,
272
270
  {
273
271
  silent = false,
272
+ environment,
274
273
  }: {
275
274
  silent?: boolean
276
- } = {},
275
+ environment: Environment
276
+ },
277
277
  ) {
278
278
  const templateDirBase = fileURLToPath(
279
279
  new URL(`../templates/${options.framework}/base`, import.meta.url),
@@ -286,15 +286,16 @@ export async function createApp(
286
286
  )
287
287
  const targetDir = resolve(process.cwd(), options.projectName)
288
288
 
289
- if (existsSync(targetDir)) {
289
+ if (environment.exists(targetDir)) {
290
290
  if (!silent) {
291
291
  log.error(`Directory "${options.projectName}" already exists`)
292
292
  }
293
293
  return
294
294
  }
295
295
 
296
- const copyFiles = createCopyFiles(targetDir)
296
+ const copyFiles = createCopyFiles(environment, targetDir)
297
297
  const templateFile = createTemplateFile(
298
+ environment,
298
299
  options.projectName,
299
300
  options,
300
301
  targetDir,
@@ -303,28 +304,23 @@ export async function createApp(
303
304
  const isAddOnEnabled = (id: string) =>
304
305
  options.chosenAddOns.find((a) => a.id === id)
305
306
 
306
- // Make the root directory
307
- await mkdir(targetDir, { recursive: true })
308
-
309
307
  // Setup the .vscode directory
310
- await mkdir(resolve(targetDir, '.vscode'), { recursive: true })
311
308
  switch (options.toolchain) {
312
309
  case 'biome':
313
- await copyFile(
310
+ await environment.copyFile(
314
311
  resolve(templateDirBase, '_dot_vscode/settings.biome.json'),
315
312
  resolve(targetDir, '.vscode/settings.json'),
316
313
  )
317
314
  break
318
315
  case 'none':
319
316
  default:
320
- await copyFile(
317
+ await environment.copyFile(
321
318
  resolve(templateDirBase, '_dot_vscode/settings.json'),
322
319
  resolve(targetDir, '.vscode/settings.json'),
323
320
  )
324
321
  }
325
322
 
326
323
  // Fill the public directory
327
- await mkdir(resolve(targetDir, 'public'), { recursive: true })
328
324
  copyFiles(templateDirBase, [
329
325
  './public/robots.txt',
330
326
  './public/favicon.ico',
@@ -333,16 +329,9 @@ export async function createApp(
333
329
  './public/logo512.png',
334
330
  ])
335
331
 
336
- // Make the src directory
337
- await mkdir(resolve(targetDir, 'src'), { recursive: true })
338
- if (options.mode === FILE_ROUTER) {
339
- await mkdir(resolve(targetDir, 'src/routes'), { recursive: true })
340
- await mkdir(resolve(targetDir, 'src/components'), { recursive: true })
341
- }
342
-
343
332
  // Check for a .cursorrules file
344
- if (existsSync(resolve(templateDirBase, '.cursorrules'))) {
345
- await copyFile(
333
+ if (environment.exists(resolve(templateDirBase, '.cursorrules'))) {
334
+ await environment.copyFile(
346
335
  resolve(templateDirBase, '.cursorrules'),
347
336
  resolve(targetDir, '.cursorrules'),
348
337
  )
@@ -388,6 +377,7 @@ export async function createApp(
388
377
 
389
378
  // Setup the package.json file, optionally with typescript, tailwind and biome
390
379
  await createPackageJSON(
380
+ environment,
391
381
  options.projectName,
392
382
  options,
393
383
  templateDirBase,
@@ -404,21 +394,24 @@ export async function createApp(
404
394
  )) {
405
395
  s?.start(`Setting up ${addOn.name}...`)
406
396
  const addOnDir = resolve(addOn.directory, 'assets')
407
- if (existsSync(addOnDir)) {
397
+ if (environment.exists(addOnDir)) {
408
398
  await copyFilesRecursively(
399
+ environment,
409
400
  addOnDir,
410
401
  targetDir,
411
- copyFile,
412
402
  async (file: string, targetFileName?: string) =>
413
403
  templateFile(addOnDir, file, targetFileName),
414
404
  )
415
405
  }
416
406
 
417
407
  if (addOn.command) {
418
- await execa(addOn.command.command, addOn.command.args || [], {
419
- cwd: targetDir,
420
- })
408
+ await environment.execute(
409
+ addOn.command.command,
410
+ addOn.command.args || [],
411
+ targetDir,
412
+ )
421
413
  }
414
+
422
415
  s?.stop(`${addOn.name} setup complete`)
423
416
  }
424
417
  }
@@ -437,9 +430,11 @@ export async function createApp(
437
430
  s?.start(
438
431
  `Installing shadcn components (${Array.from(shadcnComponents).join(', ')})...`,
439
432
  )
440
- await execa('npx', ['shadcn@canary', 'add', ...shadcnComponents], {
441
- cwd: targetDir,
442
- })
433
+ await environment.execute(
434
+ 'npx',
435
+ ['shadcn@canary', 'add', ...shadcnComponents],
436
+ targetDir,
437
+ )
443
438
  s?.stop(`Installed shadcn components`)
444
439
  }
445
440
  }
@@ -449,13 +444,13 @@ export async function createApp(
449
444
  name: string
450
445
  path: string
451
446
  }> = []
452
- if (existsSync(resolve(targetDir, 'src/integrations'))) {
453
- for (const integration of readdirSync(
447
+ if (environment.exists(resolve(targetDir, 'src/integrations'))) {
448
+ for (const integration of environment.readdir(
454
449
  resolve(targetDir, 'src/integrations'),
455
450
  )) {
456
451
  const integrationName = jsSafeName(integration)
457
452
  if (
458
- existsSync(
453
+ environment.exists(
459
454
  resolve(targetDir, 'src/integrations', integration, 'layout.tsx'),
460
455
  )
461
456
  ) {
@@ -466,7 +461,7 @@ export async function createApp(
466
461
  })
467
462
  }
468
463
  if (
469
- existsSync(
464
+ environment.exists(
470
465
  resolve(targetDir, 'src/integrations', integration, 'provider.tsx'),
471
466
  )
472
467
  ) {
@@ -477,7 +472,7 @@ export async function createApp(
477
472
  })
478
473
  }
479
474
  if (
480
- existsSync(
475
+ environment.exists(
481
476
  resolve(
482
477
  targetDir,
483
478
  'src/integrations',
@@ -499,8 +494,8 @@ export async function createApp(
499
494
  path: string
500
495
  name: string
501
496
  }> = []
502
- if (existsSync(resolve(targetDir, 'src/routes'))) {
503
- for (const file of readdirSync(resolve(targetDir, 'src/routes'))) {
497
+ if (environment.exists(resolve(targetDir, 'src/routes'))) {
498
+ for (const file of environment.readdir(resolve(targetDir, 'src/routes'))) {
504
499
  const name = file.replace(/\.tsx?|\.jsx?/, '')
505
500
  const safeRouteName = jsSafeName(name)
506
501
  routes.push({
@@ -588,7 +583,7 @@ export async function createApp(
588
583
  }
589
584
 
590
585
  // Add .gitignore
591
- await copyFile(
586
+ await environment.copyFile(
592
587
  resolve(templateDirBase, '_dot_gitignore'),
593
588
  resolve(targetDir, '.gitignore'),
594
589
  )
@@ -598,7 +593,7 @@ export async function createApp(
598
593
 
599
594
  // Install dependencies
600
595
  s?.start(`Installing dependencies via ${options.packageManager}...`)
601
- await execa(options.packageManager, ['install'], { cwd: targetDir })
596
+ await environment.execute(options.packageManager, ['install'], targetDir)
602
597
  s?.stop(`Installed dependencies`)
603
598
 
604
599
  if (warnings.length > 0) {
@@ -612,14 +607,18 @@ export async function createApp(
612
607
  switch (options.packageManager) {
613
608
  case 'pnpm':
614
609
  // pnpm automatically forwards extra arguments
615
- await execa(options.packageManager, ['run', 'check', '--fix'], {
616
- cwd: targetDir,
617
- })
610
+ await environment.execute(
611
+ options.packageManager,
612
+ ['run', 'check', '--fix'],
613
+ targetDir,
614
+ )
618
615
  break
619
616
  default:
620
- await execa(options.packageManager, ['run', 'check', '--', '--fix'], {
621
- cwd: targetDir,
622
- })
617
+ await environment.execute(
618
+ options.packageManager,
619
+ ['run', 'check', '--', '--fix'],
620
+ targetDir,
621
+ )
623
622
  break
624
623
  }
625
624
  s?.stop(`Applied toolchain ${options.toolchain}...`)
@@ -627,7 +626,7 @@ export async function createApp(
627
626
 
628
627
  if (options.git) {
629
628
  s?.start(`Initializing git repository...`)
630
- await execa('git', ['init'], { cwd: targetDir })
629
+ await environment.execute('git', ['init'], targetDir)
631
630
  s?.stop(`Initialized git repository`)
632
631
  }
633
632
 
@@ -0,0 +1,53 @@
1
+ import {
2
+ appendFile,
3
+ copyFile,
4
+ mkdir,
5
+ readFile,
6
+ writeFile,
7
+ } from 'node:fs/promises'
8
+ import { existsSync, readdirSync, statSync } from 'node:fs'
9
+ import { dirname } from 'node:path'
10
+ import { execa } from 'execa'
11
+
12
+ export type Environment = {
13
+ appendFile: (path: string, contents: string) => Promise<void>
14
+ copyFile: (from: string, to: string) => Promise<void>
15
+ writeFile: (path: string, contents: string) => Promise<void>
16
+ execute: (command: string, args: Array<string>, cwd: string) => Promise<void>
17
+
18
+ readFile: (path: string, encoding?: BufferEncoding) => Promise<string>
19
+ exists: (path: string) => boolean
20
+ readdir: (path: string) => Array<string>
21
+ isDirectory: (path: string) => boolean
22
+ }
23
+
24
+ export function createDefaultEnvironment(): Environment {
25
+ return {
26
+ appendFile: async (path: string, contents: string) => {
27
+ await mkdir(dirname(path), { recursive: true })
28
+ return appendFile(path, contents)
29
+ },
30
+ copyFile: async (from: string, to: string) => {
31
+ await mkdir(dirname(to), { recursive: true })
32
+ return copyFile(from, to)
33
+ },
34
+ writeFile: async (path: string, contents: string) => {
35
+ await mkdir(dirname(path), { recursive: true })
36
+ return writeFile(path, contents)
37
+ },
38
+ execute: async (command: string, args: Array<string>, cwd: string) => {
39
+ await execa(command, args, {
40
+ cwd,
41
+ })
42
+ },
43
+
44
+ readFile: (path: string, encoding?: BufferEncoding) =>
45
+ readFile(path, { encoding: encoding || 'utf8' }),
46
+ exists: (path: string) => existsSync(path),
47
+ readdir: (path) => readdirSync(path),
48
+ isDirectory: (path) => {
49
+ const stat = statSync(path)
50
+ return stat.isDirectory()
51
+ },
52
+ }
53
+ }
package/src/mcp.ts CHANGED
@@ -6,6 +6,7 @@ import { z } from 'zod'
6
6
 
7
7
  import { createApp } from './create-app.js'
8
8
  import { finalizeAddOns } from './add-ons.js'
9
+ import { createDefaultEnvironment } from './environment.js'
9
10
 
10
11
  const server = new McpServer({
11
12
  name: 'Demo',
@@ -112,6 +113,7 @@ server.tool(
112
113
  },
113
114
  {
114
115
  silent: true,
116
+ environment: createDefaultEnvironment(),
115
117
  },
116
118
  )
117
119
  return {
@@ -206,6 +208,7 @@ server.tool(
206
208
  },
207
209
  {
208
210
  silent: true,
211
+ environment: createDefaultEnvironment(),
209
212
  },
210
213
  )
211
214
  return {
@@ -0,0 +1,120 @@
1
+ import { useStore } from '@tanstack/react-form'
2
+ import { useFieldContext, useFormContext } from '../hooks/demo.form-context'
3
+
4
+ export function SubscribeButton({ label }: { label: string }) {
5
+ const form = useFormContext()
6
+ return (
7
+ <form.Subscribe selector={(state) => state.isSubmitting}>
8
+ {(isSubmitting) => (
9
+ <button
10
+ disabled={isSubmitting}
11
+ className="px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition-colors disabled:opacity-50"
12
+ >
13
+ {label}
14
+ </button>
15
+ )}
16
+ </form.Subscribe>
17
+ )
18
+ }
19
+
20
+ function ErrorMessages({
21
+ errors,
22
+ }: {
23
+ errors: Array<string | { message: string }>
24
+ }) {
25
+ return (
26
+ <>
27
+ {errors.map((error) => (
28
+ <div
29
+ key={typeof error === 'string' ? error : error.message}
30
+ className="text-red-500 mt-1 font-bold"
31
+ >
32
+ {typeof error === 'string' ? error : error.message}
33
+ </div>
34
+ ))}
35
+ </>
36
+ )
37
+ }
38
+
39
+ export function TextField({
40
+ label,
41
+ placeholder,
42
+ }: {
43
+ label: string
44
+ placeholder?: string
45
+ }) {
46
+ const field = useFieldContext<string>()
47
+ const errors = useStore(field.store, (state) => state.meta.errors)
48
+
49
+ return (
50
+ <div>
51
+ <label htmlFor={label} className="block font-bold mb-1 text-xl">
52
+ {label}
53
+ <input
54
+ value={field.state.value}
55
+ placeholder={placeholder}
56
+ onBlur={field.handleBlur}
57
+ onChange={(e) => field.handleChange(e.target.value)}
58
+ className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
59
+ />
60
+ </label>
61
+ {field.state.meta.isTouched && <ErrorMessages errors={errors} />}
62
+ </div>
63
+ )
64
+ }
65
+
66
+ export function TextArea({
67
+ label,
68
+ rows = 3,
69
+ }: {
70
+ label: string
71
+ rows?: number
72
+ }) {
73
+ const field = useFieldContext<string>()
74
+ const errors = useStore(field.store, (state) => state.meta.errors)
75
+
76
+ return (
77
+ <div>
78
+ <label htmlFor={label} className="block font-bold mb-1 text-xl">
79
+ {label}
80
+ <textarea
81
+ value={field.state.value}
82
+ onBlur={field.handleBlur}
83
+ rows={rows}
84
+ onChange={(e) => field.handleChange(e.target.value)}
85
+ className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
86
+ />
87
+ </label>
88
+ {field.state.meta.isTouched && <ErrorMessages errors={errors} />}
89
+ </div>
90
+ )
91
+ }
92
+
93
+ export function Select({
94
+ label,
95
+ children,
96
+ }: {
97
+ label: string
98
+ children: React.ReactNode
99
+ }) {
100
+ const field = useFieldContext<string>()
101
+ const errors = useStore(field.store, (state) => state.meta.errors)
102
+
103
+ return (
104
+ <div>
105
+ <label htmlFor={label} className="block font-bold mb-1 text-xl">
106
+ {label}
107
+ </label>
108
+ <select
109
+ name={field.name}
110
+ value={field.state.value}
111
+ onBlur={field.handleBlur}
112
+ onChange={(e) => field.handleChange(e.target.value)}
113
+ className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
114
+ >
115
+ {children}
116
+ </select>
117
+ {field.state.meta.isTouched && <ErrorMessages errors={errors} />}
118
+ </div>
119
+ )
120
+ }
@@ -0,0 +1,4 @@
1
+ import { createFormHookContexts } from '@tanstack/react-form'
2
+
3
+ export const { fieldContext, useFieldContext, formContext, useFormContext } =
4
+ createFormHookContexts()
@@ -0,0 +1,22 @@
1
+ import { createFormHook } from '@tanstack/react-form'
2
+
3
+ import {
4
+ Select,
5
+ SubscribeButton,
6
+ TextArea,
7
+ TextField,
8
+ } from '../components/demo.FormComponents'
9
+ import { fieldContext, formContext } from './demo.form-context'
10
+
11
+ export const { useAppForm } = createFormHook({
12
+ fieldComponents: {
13
+ TextField,
14
+ Select,
15
+ TextArea,
16
+ },
17
+ formComponents: {
18
+ SubscribeButton,
19
+ },
20
+ fieldContext,
21
+ formContext,
22
+ })